diff --git a/.pyup.yml b/.pyup.yml index 74c1456ce..01d4bba2a 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -21,6 +21,7 @@ search: False requirements: - requirements.txt - requirements-dev.txt + - requirements-plot.txt # configure the branch prefix the bot is using diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..dec7b44d7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,8 @@ +# .readthedocs.yml + +build: + image: latest + +python: + version: 3.6 + setup_py_install: false \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4398a1386..84f3c78d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: true os: - linux -dist: trusty +dist: xenial language: python python: - 3.6 @@ -17,9 +17,9 @@ addons: - libdw-dev - binutils-dev 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 -- 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 -e . jobs: @@ -27,7 +27,6 @@ jobs: - stage: tests script: - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - - coveralls name: pytest - script: - cp config.json.example config.json @@ -48,11 +47,13 @@ jobs: - build_helpers/publish_docker.sh name: "Build and test and push docker image" +after_success: + - coveralls notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= cache: + pip: True directories: - - $HOME/.cache/pip - - ta-lib + - /usr/local/lib diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9a967834..3c511f44d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) 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: ### 1. Run unit tests @@ -41,12 +45,6 @@ pytest freqtrade/tests/test_.py::test_ ### 2. Test if your code is PEP8 compliant -#### Install packages - -```bash -pip3.6 install flake8 coveralls -``` - #### Run Flake8 ```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 -#### Install packages - -``` bash -pip3.6 install mypy -``` - #### Run mypy ``` bash 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 ### Process: Pull Requests diff --git a/Dockerfile b/Dockerfile index 24cce0049..ded74bd18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.0-slim-stretch +FROM python:3.7.2-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential \ diff --git a/README.md b/README.md index 0d0724d3a..49b10e417 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,25 @@ -# 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) +[![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) -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) ## Disclaimer -This software is for educational purposes only. Do not risk money which -you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS -AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. +This software is for educational purposes only. Do not risk money which +you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS +AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. 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. -We strongly recommend you to have coding and Python knowledge. Do not +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. ## Exchange marketplaces supported @@ -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)) - [ ] [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 -- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux -- [x] **Persistence**: Persistence is achieved through sqlite +- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux. +- [x] **Persistence**: Persistence is achieved through sqlite. - [x] **Dry-run**: Run the bot without playing money. - [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] **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] **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] **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. - -## 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 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 ``` -_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 ### Bot commands -```bash +``` usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--dry-run-db] - {backtesting,hyperopt} ... + [--strategy-path PATH] [--customhyperopt NAME] + [--dynamic-whitelist [INT]] [--db-url PATH] + {backtesting,edge,hyperopt} ... -Simple High Frequency Trading Bot for crypto currencies +Free, open source crypto trading bot positional arguments: - {backtesting,hyperopt} + {backtesting,edge,hyperopt} backtesting backtesting module + edge edge module hyperopt hyperopt module optional arguments: -h, --help show this help message and exit - -v, --verbose be verbose - --version show program's version number and exit + -v, --verbose verbose mode (-vv for more, -vvv to get all messages) + --version show program\'s version number and exit -c PATH, --config PATH specify configuration file (default: config.json) -d PATH, --datadir PATH - path to backtest data (default: - freqtrade/tests/testdata + path to backtest data -s NAME, --strategy NAME specify strategy class name (default: DefaultStrategy) --strategy-path PATH specify additional strategy lookup path + --customhyperopt NAME + specify hyperopt class name (default: + DefaultHyperOpts) --dynamic-whitelist [INT] dynamically generate and update whitelist based on 24h - BaseVolume (Default 20 currencies) - --dry-run-db Force dry run to use a local DB - "tradesv3.dry_run.sqlite" instead of memory DB. Work - only if dry_run is enabled. + BaseVolume (default: 20) DEPRECATED. + --db-url PATH Override trades database URL, this is useful if + dry_run is enabled or in custom deployments (default: + None) ``` ### 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 - `/stop`: Stops the trader @@ -176,29 +145,29 @@ information about the bot, we encourage you to join our slack channel. ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -If you discover a bug in the bot, please -[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -first. If it hasn't been reported, please -[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and -ensure you follow the template guide so that our team can assist you as +If you discover a bug in the bot, please +[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +first. If it hasn't been reported, please +[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and +ensure you follow the template guide so that our team can assist you as quickly as possible. ### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) Have you a great idea to improve the bot you want to share? Please, first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). -If it hasn't been requested, please -[create a new request](https://github.com/freqtrade/freqtrade/issues/new) -and ensure you follow the template guide so that it does not get lost +If it hasn't been requested, please +[create a new request](https://github.com/freqtrade/freqtrade/issues/new) +and ensure you follow the template guide so that it does not get lost in the bug reports. ### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) Feel like our bot is missing a feature? We welcome your pull requests! -Please read our +Please read our [Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) -to understand the requirements before sending your pull-requests. +to understand the requirements before sending your pull-requests. Coding is not a neccessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. @@ -221,10 +190,9 @@ To run this bot we recommend you a cloud instance with a minimum of: ### Software requirements -- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) - [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) \ No newline at end of file diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 4d4f37c17..9fe341bba 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -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 cd ta-lib \ && 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 \ && cd .. else - echo "TA-lib already installed, skipping download and build." - cd ta-lib && sudo make install && cd .. - + echo "TA-lib already installed, skipping installation" fi diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 9d82fc2d5..7a8127c44 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -13,7 +13,7 @@ if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then else echo "event ${TRAVIS_EVENT_TYPE}: building with cache" # 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} . fi diff --git a/config_full.json.example b/config_full.json.example index 0427f8700..23a36dd4c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -3,6 +3,7 @@ "stake_currency": "BTC", "stake_amount": 0.05, "fiat_display_currency": "USD", + "amount_reserve_percent" : 0.05, "dry_run": false, "ticker_interval": "5m", "trailing_stop": false, @@ -37,7 +38,8 @@ "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": "false" + "stoploss_on_exchange": "false", + "stoploss_on_exchange_interval": 60 }, "order_time_in_force": { "buy": "gtc", diff --git a/docs/backtesting.md b/docs/backtesting.md index cc8ecd6c7..f6c9dd4d1 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -1,24 +1,19 @@ # Backtesting -This page explains how to validate your strategy performance by using +This page explains how to validate your strategy performance by using 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 Now you have good Buy and Sell strategies, you want to test it against -real data. This is what we call +real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pair) from your config file -and load static tickers located in -[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). -If the 5 min and 1 min ticker for the crypto-currencies to test is not -already in the `testdata` folder, backtesting will download them +and load static tickers located in +[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). +If the 5 min and 1 min ticker for the crypto-currencies to test is not +already in the `testdata` folder, backtesting will download them automatically. Testdata files will not be updated until you specify it. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. @@ -171,60 +166,72 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -======================================== BACKTESTING REPORT ========================================= -| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss | -|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:| -| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 | -| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 | -| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 | -| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 | -| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 | -| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 | -| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 | -| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 | -| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 | -| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 | -| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 | -2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO - -====================================== LEFT OPEN TRADES REPORT ====================================== -| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss | -|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:| -| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 | -| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 | -| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 | -| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | -| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 | -| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 | -| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | -| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 | -| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 | -| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 | -| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 | - +========================================================= BACKTESTING REPORT ======================================================== +| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | +|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| +| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 21 | +| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 8 | +| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 14 | +| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 7 | +| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 10 | +| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 20 | +| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 15 | +| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 17 | +| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 18 | +| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 9 | +| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 21 | +| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 7 | +| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 13 | +| 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 | +| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 11 | +| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 23 | +| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 | +| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | +========================================================= SELL REASON STATS ========================================================= +| Sell Reason | Count | +|:-------------------|--------:| +| trailing_stop_loss | 205 | +| stop_loss | 166 | +| sell_signal | 56 | +| 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 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. The last line will give you the overall performance of your strategy, 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 -`52.9` min, with a performance of `-0.41%` (loss), that means it has -lost a total of `-0.00348593 BTC`. - -As you will see your strategy performance will be influenced by your buy -strategy, your sell strategy, and also by the `minimal_roi` and -`stop_loss` you have set. +We understand the bot has made `429` trades for an average duration of +`4:12:00`, with a performance of `76.20%` (profit), that means it has +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 +strategy, your sell strategy, and also by the `minimal_roi` and +`stop_loss` you have set. As for an example if your minimal_roi is only `"0": 0.01`. You cannot -expect the bot to make more profit than 1% (because it will sell every +expect the bot to make more profit than 1% (because it will sell every time a trade will reach 1%). ```json @@ -234,21 +241,21 @@ time a trade will reach 1%). ``` On the other hand, if you set a too high `minimal_roi` like `"0": 0.55` -(55%), there is a lot of chance that the bot will never reach this -profit. Hence, keep in mind that your performance is a mix of your +(55%), there is a lot of chance that the bot will never reach this +profit. Hence, keep in mind that your performance is a mix of your strategies, your configuration, and the crypto-currency you have set up. ## Backtesting multiple strategies To backtest multiple strategies, a list of Strategies can be provided. -This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple +This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this should give a nice runtime boost. All listed Strategies need to be in the same folder. ``` bash -freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades +freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades ``` This will save the results to `user_data/backtest_data/backtest-result-.json`, injecting the strategy-name into the target filename. @@ -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. ``` -=================================================== Strategy Summary ==================================================== -| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss | -|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:| -| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 | -| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 | +=========================================================== Strategy Summary =========================================================== +| Strategy | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | +|:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| +| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | +| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 | ``` ## Next step Great, your strategy is profitable. What if the bot can give your the -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) +optimal parameters to use for your strategy? +Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md) diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 0fc41a2e3..8592f6cca 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -1,28 +1,8 @@ -# Bot Optimization +# Optimization This page explains where to customize your strategies, and add new 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 This is very simple. Copy paste your strategy file into the folder @@ -60,13 +40,19 @@ A strategy file contains all the information needed to build a good strategy: The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. You can test it with the parameter: `--strategy TestStrategy` -``` bash +```bash 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) 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 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,10 +104,10 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> 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). -Then uncomment indicators you need. +!!! 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).
+ Then uncomment indicators you need. ### Buy signal rules @@ -187,7 +173,7 @@ This dict defines the minimal Return On Investment (ROI) a trade should reach be It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage. -```python +```python minimal_roi = { "40": 0.0, "30": 0.01, @@ -199,10 +185,9 @@ minimal_roi = { The above configuration would therefore mean: - Sell whenever 4% profit was reached -- Sell after 20 minutes when 2% profit was reached -- Sell after 20 minutes when 2% profit was reached -- Sell after 30 minutes when 1% profit was reached -- Sell after 40 minutes when the trade is non-loosing (no profit) +- Sell when 2% profit was reached (in effect after 20 minutes) +- Sell when 1% profit was reached (in effect after 30 minutes) +- Sell when trade is non-loosing (in effect after 40 minutes) The calculation does include fees. @@ -227,7 +212,7 @@ stoploss = -0.10 ``` This would signify a stoploss of -10%. -If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). +If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). For more information on order_types please look [here](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md#understand-order_types). @@ -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. 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 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`. +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? 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 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). diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 5451c8459..96b16b6b6 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -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 ``` -usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] - {backtesting,hyperopt} ... +usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] + [--strategy-path PATH] [--customhyperopt NAME] + [--dynamic-whitelist [INT]] [--db-url PATH] + {backtesting,edge,hyperopt} ... -Simple High Frequency Trading Bot for crypto currencies +Free, open source crypto trading bot positional arguments: - {backtesting,hyperopt} + {backtesting,edge,hyperopt} backtesting backtesting module + edge edge module hyperopt hyperopt module optional arguments: -h, --help show this help message and exit - -v, --verbose be verbose - --version show program's version number and exit + -v, --verbose verbose mode (-vv for more, -vvv to get all messages) + --version show program\'s version number and exit -c PATH, --config PATH specify configuration file (default: config.json) -d PATH, --datadir PATH @@ -34,12 +30,15 @@ optional arguments: -s NAME, --strategy NAME specify strategy class name (default: DefaultStrategy) --strategy-path PATH specify additional strategy lookup path + --customhyperopt NAME + specify hyperopt class name (default: + DefaultHyperOpts) --dynamic-whitelist [INT] 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 dry_run is enabled or in custom deployments (default: - sqlite:///tradesv3.sqlite) + None) ``` ### 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 ``` -### How to use --strategy? +### How to use **--strategy**? This parameter will allow you to load your custom strategy class. 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). -### How to use --strategy-path? +### How to use **--strategy-path**? 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!): @@ -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 `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 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 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 stored in a database. If you want to store your bot actions in a DB @@ -129,15 +129,17 @@ python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.s Backtesting also uses the config specified via `-c/--config`. ``` -usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] - [--timerange TIMERANGE] [-l] [-r] - [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] - [--export EXPORT] [--export-filename PATH] +usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [--eps] [--dmmp] [-l] [-r] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + [--export EXPORT] [--export-filename PATH] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL specify ticker interval (1m, 5m, 30m, 1h, 1d) + --timerange TIMERANGE + specify what timerange of data to use. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking) @@ -145,8 +147,6 @@ optional arguments: Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number) - --timerange TIMERANGE - specify what timerange of data to use. -l, --live using live data -r, --refresh-pairs-cached refresh the pairs files in tests/testdata with the @@ -167,18 +167,18 @@ optional arguments: filename=user_data/backtest_data/backtest_today.json (default: user_data/backtest_data/backtest- 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 set in your config file and download data from Bittrex. 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. -**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 the parameter `-l` or `--live`. @@ -250,4 +250,4 @@ in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc. ## Next step 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). diff --git a/docs/configuration.md b/docs/configuration.md index 6f54cfa8d..108e264c6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,12 +2,6 @@ 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 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. -| Command | Default | Mandatory | Description | -|----------|---------|----------|-------------| -| `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. +Mandatory Parameters are marked as **Required**. -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 `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 account an exception is generated. -To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`. -In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`. +To allow the bot to trade all the available `stake_currency` in your account set + +```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 @@ -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. See the example below: -``` +```json "minimal_roi": { "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 @@ -144,26 +166,31 @@ end up paying more then would probably have been necessary. ### 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. -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. -``` python - "order_types": { - "buy": "limit", - "sell": "limit", - "stoploss": "market", - "stoploss_on_exchange": False - }, +```python +"order_types": { + "buy": "limit", + "sell": "limit", + "stoploss": "market", + "stoploss_on_exchange": False, + "stoploss_on_exchange_interval": 60 +}, ``` -**NOTE**: Not all exchanges support "market" orders. -The following message will be shown if your exchange does not support market orders: `"Exchange does not 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 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 -Order time in force defines the policy by which the order is executed on the exchange. Three commonly used time in force are:
+`order_time_in_force` defines the policy by which the order is executed on the exchange. Three commonly used time in force are:
**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.
**FOK (Full Or Kill):** @@ -174,12 +201,14 @@ It is the same as FOK (above) except it can be partially fulfilled. The remainin `order_time_in_force` contains a dict buy and sell time in force policy. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
possible values are: `gtc` (default), `fok` or `ioc`.
``` python - "order_time_in_force": { - "buy": "gtc", - "sell": "gtc" - }, +"order_time_in_force": { + "buy": "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.
+ +!!! 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? @@ -198,9 +227,15 @@ Feel free to test other exchanges and submit your PR to improve the bot. ### What values for fiat_display_currency? `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". -In addition to central bank currencies, a range of cryto currencies are supported. -The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT". +The valid values are:
+```json +"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 @@ -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 creating trades. -### To switch your bot in Dry-run mode: - 1. Edit your `config.json` file 2. Switch dry-run to true and specify db_url for a persistent db ```json "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) @@ -238,9 +271,9 @@ production mode. Dynamic pairlists select pairs for you based on the logic configured. The bot runs against all pairs (with that stake) on the exchange, and a number of assets (`number_assets`) is selected based on the selected criteria. -By *default*, 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"` * 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 -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 "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 "exchange": { @@ -285,8 +318,8 @@ you run it in production mode. } ``` - -If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md). +!!! Note + If you have an exchange API key yet, [see our tutorial](/pre-requisite). ### Using proxy with FreqTrade @@ -337,4 +370,4 @@ Please ensure that 'NameOfStrategy' is identical to the strategy name! ## 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). diff --git a/docs/developer.md b/docs/developer.md index 9137f16ca..6fbcdc812 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -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. +## 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 @@ -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. 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`) diff --git a/docs/edge.md b/docs/edge.md index 829910484..b208cb318 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -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. -**NOTICE:** Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist. -**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. +!!! Warning + Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist. -## Table of Contents - -- [Introduction](#introduction) -- [How does it work?](#how-does-it-work?) -- [Configurations](#configurations) -- [Running Edge independently](#running-edge-independently) +!!! 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 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.

@@ -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). -`W = (Number of winning trades) / (Number of losing trades)` +`W = (Number of winning trades) / (Total number of trades)` ### 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: @@ -213,4 +209,4 @@ The full timerange specification: * Use tickframes till 2018/01/31: --timerange=-20180131 * Use tickframes since 2018/01/31: --timerange=20180131- * Use tickframes since 2018/01/31 till 2018/03/01 : --timerange=20180131-20180301 -* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600 \ No newline at end of file +* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600 diff --git a/docs/faq.md b/docs/faq.md index 31a302067..4bbf28fe6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,43 +2,43 @@ #### I have waited 5 minutes, why hasn't the bot made any trades yet?! -Depending on the buy strategy, the amount of whitelisted coins, the -situation of the market etc, it can take up to hours to find good entry +Depending on the buy strategy, the amount of whitelisted coins, the +situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient! #### I have made 12 trades already, why is my total profit negative?! -I understand your disappointment but unfortunately 12 trades is just -not enough to say anything. If you run backtesting, you can see that our -current algorithm does leave you on the plus side, but that is after -thousands of trades and even there, you will be left with losses on -specific coins that you have traded tens if not hundreds of times. We -of course constantly aim to improve the bot but it will _always_ be a -gamble, which should leave you with modest wins on monthly basis but +I understand your disappointment but unfortunately 12 trades is just +not enough to say anything. If you run backtesting, you can see that our +current algorithm does leave you on the plus side, but that is after +thousands of trades and even there, you will be left with losses on +specific coins that you have traded tens if not hundreds of times. We +of course constantly aim to improve the bot but it will _always_ be a +gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -#### I’d like to change the stake amount. Can I just stop the bot with +#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? -Not quite. Trades are persisted to a database but the configuration is -currently only read when the bot is killed and restarted. `/stop` more +Not quite. Trades are persisted to a database but the configuration is +currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again. #### I want to improve the bot with a new strategy -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). +That's great. We have a nice backtesting and hyperoptimizing setup. See +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? -You can use the `/forcesell all` command from Telegram. +You can use the `/forcesell all` command from Telegram. ### How many epoch do I need to get a good Hyperopt result? -Per default Hyperopts without `-e` or `--epochs` parameter will only -run 100 epochs, means 100 evals of your triggers, guards, .... Too few -to find a great result (unless if you are very lucky), so you probably -have to run it for 10.000 or more. But it will take an eternity to +Per default Hyperopts without `-e` or `--epochs` parameter will only +run 100 epochs, means 100 evals of your triggers, guards, .... Too few +to find a great result (unless if you are very lucky), so you probably +have to run it for 10.000 or more. But it will take an eternity to compute. We recommend you to run it at least 10.000 epochs: @@ -52,7 +52,7 @@ for i in {1..100}; do python3 ./freqtrade/main.py hyperopt -e 100; done ``` #### Why it is so long to run hyperopt? -Finding a great Hyperopt results takes time. +Finding a great Hyperopt results takes time. If you wonder why it takes a while to find great hyperopt results @@ -60,12 +60,11 @@ This answer was written during the under the release 0.15.1, when we had : - 8 triggers - 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to +- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise -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. -Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th +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. +Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space. - diff --git a/docs/hyperopt.md b/docs/hyperopt.md index dffe84d1d..0c18110bd 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,69 +1,87 @@ # Hyperopt -This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses several +This page explains how to tune your strategy by finding the optimal +parameters, a process called hyperparameter optimization. The bot uses several 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 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) - -## 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) +!!! Bug + Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) ## Prepare Hyperopting -Before we start digging in 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) +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/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 -This is very simple. Put your hyperopt file into the folder -`user_data/hyperopts`. -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` +Put your hyperopt file into the folder`user_data/hyperopts`. + +Let assume you want a hyperopt file `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 -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). + +There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: + +- 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`. -1. Guards are conditions like "never buy if ADX < 10", or never buy if -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". +1. Guards are conditions like "never buy if ADX < 10", or never buy if 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". -Hyperoptimization will, for each eval round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like -"*buy exactly when close price touches lower bollinger band, BUT only if +Hyperoptimization will, for each eval round, pick one trigger and possibly +multiple guards. The constructed strategy will be something like +"*buy exactly when close price touches lower bollinger band, BUT only if ADX > 10*". 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. +#### 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 -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this +Let's say you are curious: should you use MACD crossings or lower Bollinger +Bands to trigger your buys. And you also wonder should you use RSI or ADX to +help with those buy decisions. If you decide to use RSI or ADX, which values +should I use for them? So let's use hyperparameter optimization to solve this mystery. We will start by defining a search space: -``` +```python def indicator_space() -> List[Dimension]: """ Define your Hyperopt space for searching strategy parameters @@ -77,8 +95,8 @@ We will start by defining a search space: ] ``` -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` +Above definition says: I have five parameters I want you to randomly combine +to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last @@ -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: -``` +``` python def populate_buy_trend(dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS @@ -96,12 +114,13 @@ So let's write the buy strategy using these values: conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -117,12 +136,12 @@ with different value combinations. It will then use the given historical data an buys based on the buy signals generated with the above function and based on the results it will end with telling you which paramter combination produced the best profits. -The search for best parameters starts with a few random combinations and then uses a +The search for best parameters starts with a few random combinations and then uses a regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`. The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. -When you want to test an indicator that isn't used by the bot currently, remember to +When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in `hyperopt.py`. ## Execute Hyperopt @@ -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. ```bash -python3 ./freqtrade/main.py -s --hyperopt -c config.json hyperopt -e 5000 +python3 ./freqtrade/main.py --hyperopt -c config.json hyperopt -e 5000 --spaces all ``` -Use `` and `` as the names of the custom strategy -(only required for generating sells) and the custom hyperopt used. +Use `` as the name of the custom hyperopt used. The `-e` flag will set how many evaluations hyperopt will do. We recommend 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 If you would like to hyperopt parameters using an alternate ticker data that @@ -161,15 +184,16 @@ python3 ./freqtrade/main.py hyperopt --timerange -200 ### Running Hyperopt with Smaller Search Space Use the `--spaces` argument to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. Often it -might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome +Letting Hyperopt optimize everything is a huuuuge search space. Often it +might make more sense to start by just searching for initial buy algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. Legal values are: - `all`: optimize everything - `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 - `stoploss`: search for the best stoploss value - space-separated list of any of the above values for example `--spaces roi stoploss` @@ -183,25 +207,29 @@ Given the following result from hyperopt: Best result: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. 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: - The buy trigger that worked best was `bb_lower`. -- You should not use ADX because `adx-enabled: False`) +- You should not use ADX because `adx-enabled: False`) - You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) -You have to look inside your strategy file into `buy_strategy_generator()` +You have to look inside your strategy file into `buy_strategy_generator()` method, what those values match to. - + So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: ``` (dataframe['rsi'] < 29.0) ``` - -Translating your whole hyperopt result as the new buy-signal + +Translating your whole hyperopt result as the new buy-signal would then look like: ```python @@ -223,9 +251,24 @@ If you are optimizing ROI, you're result will look as follows and include a ROI Best result: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. 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: -{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: @@ -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`. 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). -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. +!!! 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. ## Next Step 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). diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 000000000..c7138e84b Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/index.md b/docs/index.md index 8fa24e996..92ea4fe43 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 -this documentation if you see it became outdated by sending us a -Pull-request. Do not hesitate to reach us on -[Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) - if you do not find the answer to your questions. + +Star + +Fork + +Download + +Follow @freqtrade +## 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) - - [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) -- [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) - - [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation) - - [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows) -- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) -- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md) - - [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) - - [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands) - - [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands) - - [Edge commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#edge-commands) -- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - - [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy) - - [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator) - - [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - - [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) -- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md) -- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - - [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) -- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) - - [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) -- [Developer Docs](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.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. + + 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. + + +## Features + - Based on Python 3.6+: For botting on any operating system - Windows, macOS and Linux + - Persistence: Persistence is achieved through sqlite + - Dry-run: Run the bot without playing money. + - Backtesting: Run a simulation of your buy/sell strategy. + - Strategy Optimization by machine learning: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. + - 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 + - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists. + - Blacklist crypto-currencies: Select which crypto-currency you want to avoid. + - Manageable via Telegram: Manage the bot with Telegram + - Display profit/loss in fiat: Display your profit/loss in 33 fiat. + - Daily summary of profit/loss: Provide a daily summary of your profit/loss. + - Performance status report: Provide a performance status of your current trades. + + +## Requirements +### Uptodate clock +The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges. + +### Hardware requirements +To run this bot we recommend you a cloud instance with a minimum of: + +- 2GB RAM +- 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). \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index 71ae5a6b5..80223f954 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,25 +1,69 @@ # Installation - 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) -* [Easy Installation - Linux Script](#easy-installation---linux-script) -* [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) +### Setup your exchange 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.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)**
+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`.** +
+## 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). +
## 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. @@ -33,7 +77,7 @@ usage: -c,--config Easy config generator (Will override your existing file). ``` -### --install +** --install ** 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` -### --update +** --update ** 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. -### --config +** --config ** 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.1. Clone the git repository +**1.1. Clone the git repository** +Linux/Mac/Windows with WSL ```bash 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 git checkout develop ``` -#### 1.3. Go into the new directory +**1.3. Go into the new directory** ```bash cd freqtrade ``` -#### 1.4. Copy `config.json.example` to `config.json` +**1.4. Copy `config.json.example` to `config.json`** ```bash 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 @@ -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/). -#### 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 @@ -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. -#### 2.2. Build the Docker image +**2.2. Build the Docker image** ```bash 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 ``` -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. @@ -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). -#### 5.1. Move your config file and database +**5.1. Move your config file and database** ```bash mkdir ~/.freqtrade @@ -180,7 +230,7 @@ mv config.json ~/.freqtrade mv tradesv3.sqlite ~/.freqtrade ``` -#### 5.2. Run the docker image +**5.2. Run the docker image** ```bash docker run -d \ @@ -191,8 +241,9 @@ docker run -d \ 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. -To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` +!!! 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` ### 6. Monitor your Docker instance @@ -208,14 +259,15 @@ docker start freqtrade 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 The following assumes that the above steps (1-4) have been completed successfully. Also, backtest-data should be available at `~/.freqtrade/user_data/`. -``` bash +```bash docker run -d \ --name freqtrade \ -v /etc/localtime:/etc/localtime:ro \ @@ -225,16 +277,17 @@ docker run -d \ 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 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 @@ -286,7 +339,7 @@ python3 -m pip install -e . brew install python3 git wget ``` -### common +### Common #### 1. Install TA-Lib @@ -304,11 +357,13 @@ cd .. 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) -*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 python3 -m venv .env @@ -337,7 +392,7 @@ cd freqtrade 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 @@ -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). -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_Lib‑0.4.17‑cp36‑cp36m‑win32.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_Lib‑0.4.17‑cp36‑cp36m‑win32.whl` (make sure to use the version matching your python version) ```cmd >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 -[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)... +[Bot Configuration](configuration.md). diff --git a/docs/partials/header.html b/docs/partials/header.html new file mode 100644 index 000000000..84c254b5a --- /dev/null +++ b/docs/partials/header.html @@ -0,0 +1,52 @@ +
+ + + +
\ No newline at end of file diff --git a/docs/plotting.md b/docs/plotting.md index 54a4bb4b8..a9b191e75 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -1,10 +1,6 @@ # Plotting 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 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: ``` -script/plot_dataframe.py [-h] [-p pair] [--live] +script/plot_dataframe.py [-h] [-p pairs] [--live] ``` Example @@ -27,11 +23,16 @@ Example python scripts/plot_dataframe.py -p BTC/ETH ``` -The `-p` pair argument, can be used to specify what -pair you would like to plot. +The `-p` pairs argument, can be used to specify +pairs you would like to plot. **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: ``` python scripts/plot_dataframe.py -p BTC/ETH --live @@ -48,7 +49,7 @@ To plot trades stored in a database use `--db-url` argument: python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` -To plot a test strategy the strategy should have first be backtested. +To plot a test strategy the strategy should have first be backtested. The results may then be plotted with the -s argument: ``` python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data// diff --git a/docs/pre-requisite.md b/docs/pre-requisite.md deleted file mode 100644 index 79232a89d..000000000 --- a/docs/pre-requisite.md +++ /dev/null @@ -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`.** - diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 000000000..c4d8c2cae --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1 @@ +mkdocs-material==3.1.0 \ No newline at end of file diff --git a/docs/stoploss.md b/docs/stoploss.md index f34050a85..0726aebbc 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -2,9 +2,20 @@ At this stage the bot contains the following stoploss support modes: -1. static stop loss, defined in either the strategy or configuration -2. trailing stop loss, defined in the configuration -3. trailing stop loss, custom positive loss, defined in configuration +1. static stop loss, defined in either the strategy or configuration. +2. trailing stop loss, defined in the 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 diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 28213fb5d..e01c8f9bc 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -2,14 +2,14 @@ This page explains how to command your bot with Telegram. -## Pre-requisite -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) +## Prerequisite +To control your bot with Telegram, you need first to +[set up a Telegram bot](installation.md) and add your Telegram API keys into your config file. ## Telegram commands -Per default, the Telegram bot shows predefined commands. Some commands -are only available by sending them to the bot. The table below list the +Per default, the Telegram bot shows predefined commands. Some commands +are only available by sending them to the bot. The table below list the official commands. You can ask at any moment for help with `/help`. | Command | Default | Description | @@ -40,30 +40,30 @@ Below, example of Telegram message you will receive for each command. ### /stop -> `Stopping trader ...` +> `Stopping trader ...` > **Status:** `stopped` ## /status For each open trade, the bot will send you the following message. -> **Trade ID:** `123` +> **Trade ID:** `123` > **Current Pair:** CVC/BTC -> **Open Since:** `1 days ago` -> **Amount:** `26.64180098` -> **Open Rate:** `0.00007489` -> **Close Rate:** `None` -> **Current Rate:** `0.00007489` -> **Close Profit:** `None` -> **Current Profit:** `12.95%` +> **Open Since:** `1 days ago` +> **Amount:** `26.64180098` +> **Open Rate:** `0.00007489` +> **Close Rate:** `None` +> **Current Rate:** `0.00007489` +> **Close Profit:** `None` +> **Current Profit:** `12.95%` > **Open Order:** `None` ## /status table Return the status of all open trades in a table format. ``` - ID Pair Since Profit ----- -------- ------- -------- + ID Pair Since Profit +---- -------- ------- -------- 67 SC/BTC 1 d 13.33% 123 CVC/BTC 1 h 12.95% ``` @@ -73,32 +73,32 @@ Return the status of all open trades in a table format. Return the number of trades used and available. ``` current max ---------- ----- - 2 10 +--------- ----- + 2 10 ``` ## /profit Return a summary of your profit/loss and performance. -> **ROI:** Close trades -> ∙ `0.00485701 BTC (258.45%)` -> ∙ `62.968 USD` -> **ROI:** All trades -> ∙ `0.00255280 BTC (143.43%)` -> ∙ `33.095 EUR` -> -> **Total Trade Count:** `138` -> **First Trade opened:** `3 days ago` -> **Latest Trade opened:** `2 minutes ago` -> **Avg. Duration:** `2:33:45` +> **ROI:** Close trades +> ∙ `0.00485701 BTC (258.45%)` +> ∙ `62.968 USD` +> **ROI:** All trades +> ∙ `0.00255280 BTC (143.43%)` +> ∙ `33.095 EUR` +> +> **Total Trade Count:** `138` +> **First Trade opened:** `3 days ago` +> **Latest Trade opened:** `2 minutes ago` +> **Avg. Duration:** `2:33:45` > **Best Performing:** `PAY/BTC: 50.23%` ## /forcesell > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` -## /forcebuy +## /forcebuy > **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) @@ -107,7 +107,7 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. ## /performance Return the performance of each crypto-currency the bot has sold. -> Performance: +> Performance: > 1. `RCN/BTC 57.77%` > 2. `PAY/BTC 56.91%` > 3. `VIB/BTC 47.07%` @@ -119,31 +119,30 @@ Return the performance of each crypto-currency the bot has sold. Return the balance of all crypto-currency your have on the exchange. -> **Currency:** BTC -> **Available:** 3.05890234 -> **Balance:** 3.05890234 -> **Pending:** 0.0 +> **Currency:** BTC +> **Available:** 3.05890234 +> **Balance:** 3.05890234 +> **Pending:** 0.0 -> **Currency:** CVC -> **Available:** 86.64180098 -> **Balance:** 86.64180098 +> **Currency:** CVC +> **Available:** 86.64180098 +> **Balance:** 86.64180098 > **Pending:** 0.0 ## /daily -Per default `/daily` will return the 7 last days. +Per default `/daily` will return the 7 last days. The example below if for `/daily 3`: > **Daily Profit over the last 3 days:** ``` -Day Profit BTC Profit USD ----------- -------------- ------------ -2018-01-03 0.00224175 BTC 29,142 USD -2018-01-02 0.00033131 BTC 4,307 USD +Day Profit BTC Profit USD +---------- -------------- ------------ +2018-01-03 0.00224175 BTC 29,142 USD +2018-01-02 0.00033131 BTC 4,307 USD 2018-01-01 0.00269130 BTC 34.986 USD ``` ## /version -> **Version:** `0.14.3` - +> **Version:** `0.14.3` diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2319135c6..0aa01211d 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '0.18.0' +__version__ = '0.18.1' class DependencyException(BaseException): diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 19cccfe8b..9b1b9a925 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -272,7 +272,7 @@ class Arguments(object): '-s', '--spaces', help='Specify which parameters to hyperopt. Space separate list. \ Default: %(default)s', - choices=['all', 'buy', 'roi', 'stoploss'], + choices=['all', 'buy', 'sell', 'roi', 'stoploss'], default='all', nargs='+', dest='spaces', @@ -352,9 +352,9 @@ class Arguments(object): Parses given arguments for scripts. """ self.parser.add_argument( - '-p', '--pair', + '-p', '--pairs', help='Show profits for only this pairs. Pairs are comma-separated.', - dest='pair', + dest='pairs', default=None ) diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 5c7da3413..d972f50b8 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -12,6 +12,7 @@ from jsonschema import Draft4Validator, validate from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants +from freqtrade.state import RunMode 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 """ - def __init__(self, args: Namespace) -> None: + def __init__(self, args: Namespace, runmode: RunMode = None) -> None: self.args = args self.config: Optional[Dict[str, Any]] = None + self.runmode = runmode def load_config(self) -> Dict[str, Any]: """ @@ -68,6 +70,13 @@ class Configuration(object): # Load Hyperopt 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 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: config.update({'db_url': self.args.db_url}) 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): logger.info('Dry run is enabled') @@ -152,13 +158,16 @@ class Configuration(object): return config - def _create_default_datadir(self, config: Dict[str, Any]) -> str: - exchange_name = config.get('exchange', {}).get('name').lower() - default_path = os.path.join('user_data', 'data', exchange_name) - if not os.path.isdir(default_path): - os.makedirs(default_path) - logger.info(f'Created data directory: {default_path}') - return default_path + 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() + datadir = os.path.join('user_data', 'data', exchange_name) + + if not os.path.isdir(datadir): + os.makedirs(datadir) + logger.info(f'Created data directory: {datadir}') + return datadir 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' in self.args and self.args.datadir: - config.update({'datadir': self.args.datadir}) + config.update({'datadir': self._create_datadir(config, self.args.datadir)}) 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')) # If -r/--refresh-pairs-cached is used we add it to the configuration diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b2393c2b7..6c71ddf7b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -13,6 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite://' UNLIMITED_STAKE_AMOUNT = 'unlimited' +DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] @@ -112,7 +113,8 @@ CONF_SCHEMA = { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'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'] }, @@ -137,7 +139,7 @@ CONF_SCHEMA = { 'pairlist': { 'type': 'object', 'properties': { - 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, + 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, 'config': {'type': 'object'} }, 'required': ['method'] diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index ee915ec19..c32338bbe 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -5,13 +5,19 @@ import logging import pandas as pd from pandas import DataFrame, to_datetime +from freqtrade.constants import TICKER_INTERVAL_MINUTES + 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 :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 """ logger.debug("Parsing tickerlist to dataframe") @@ -23,6 +29,12 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame: utc=True, infer_datetime_format=True) + # Some exchanges return int values for volume and even for ohlc. + # Convert them since TA-LIB indicators used in the strategy assume floats + # 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 frame = frame.groupby(by='date', as_index=False, sort=True).agg({ 'open': 'first', @@ -33,7 +45,41 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame: }) frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle logger.debug('Dropping last candle') - return frame + + if fill_missing: + return ohlcv_fill_up_missing_data(frame, ticker_interval) + else: + 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: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py new file mode 100644 index 000000000..375b8bf5b --- /dev/null +++ b/freqtrade/data/dataprovider.py @@ -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)) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index f4ff46a1a..7d89f7ad6 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -5,15 +5,12 @@ includes: * download data from exchange and store to disk """ -import gzip - import logging from pathlib import Path from typing import Optional, List, Dict, Tuple, Any import arrow from pandas import DataFrame -import ujson from freqtrade import misc, constants, OperationalException from freqtrade.data.converter import parse_ticker_dataframe @@ -23,15 +20,6 @@ from freqtrade.arguments import TimeRange 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]: """ Trim tickerlist based on given timerange @@ -77,18 +65,10 @@ def load_tickerdata_file( path = make_testdata_path(datadir) pair_s = pair.replace('/', '_') file = path.joinpath(f'{pair_s}-{ticker_interval}.json') - 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: + pairdata = misc.file_load_json(file) + + if not pairdata: return None if timerange: @@ -102,26 +82,28 @@ def load_pair_history(pair: str, timerange: TimeRange = TimeRange(None, None, 0, 0), refresh_pairs: bool = False, exchange: Optional[Exchange] = None, + fill_up_missing: bool = True ) -> DataFrame: """ Loads cached ticker history for the given pair. :return: DataFrame with ohlcv data """ - pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) # If the user force the refresh of pairs if refresh_pairs: if not exchange: raise OperationalException("Exchange needs to be initialized when " "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, exchange=exchange, pair=pair, tick_interval=ticker_interval, timerange=timerange) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) + if pairdata: if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000: 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', pair, 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: logger.warning('No data for pair: "%s", Interval: %s. ' 'Use --refresh-pairs-cached to download the data', @@ -143,7 +125,8 @@ def load_data(datadir: Optional[Path], pairs: List[str], refresh_pairs: bool = False, 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 :return: dict(:) @@ -154,7 +137,8 @@ def load_data(datadir: Optional[Path], hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, datadir=datadir, timerange=timerange, refresh_pairs=refresh_pairs, - exchange=exchange) + exchange=exchange, + fill_up_missing=fill_up_missing) if hist is not None: result[pair] = hist return result @@ -185,7 +169,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, # read the cached file if filename.is_file(): with open(filename, "rt") as file: - data = json_load(file) + data = misc.json_load(file) # remove the last item, could be incomplete candle if data: data.pop() @@ -246,6 +230,6 @@ def download_pair_history(datadir: Optional[Path], misc.file_dump_json(filename, data) return True except BaseException: - logger.info('Failed to download the pair: "%s", Interval: %s', - pair, tick_interval) - return False + logger.info('Failed to download the pair: "%s", Interval: %s', + pair, tick_interval) + return False diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 8ccfc90de..baef811de 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -59,7 +59,7 @@ class Edge(): # checking max_open_trades. it should be -1 as with Edge # 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 !') if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: @@ -190,9 +190,16 @@ class Edge(): if self._final_pairs != final: self._final_pairs = final 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: - 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 diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 98522b98a..47886989e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -80,10 +80,10 @@ class Exchange(object): self._cached_ticker: Dict[str, Any] = {} # 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 - self._klines: Dict[str, DataFrame] = {} + self._klines: Dict[Tuple[str, str], DataFrame] = {} # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} @@ -158,11 +158,12 @@ class Exchange(object): """exchange ccxt id""" return self._api.id - def klines(self, pair: str, copy=True) -> DataFrame: - if pair in self._klines: - return self._klines[pair].copy() if copy else self._klines[pair] + def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: + # create key tuple + if pair_interval in self._klines: + return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] else: - return None + return DataFrame() def set_sandbox(self, api, exchange_config: dict, name: str): if exchange_config.get('sandbox'): @@ -236,7 +237,7 @@ class Exchange(object): f'Exchange {self.name} does not support market orders.') if order_types.get('stoploss_on_exchange'): - if self.name is not 'Binance': + if self.name != 'Binance': raise OperationalException( '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 """ 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( 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] try: - return self._api.create_order(pair, 'stop_loss_limit', 'sell', - amount, rate, {'stopPrice': stop_price}) + order = self._api.create_order(pair, 'stop_loss_limit', 'sell', + 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: raise DependencyException( @@ -515,58 +519,68 @@ class Exchange(object): input_coroutines = [self._async_get_candle_history( pair, tick_interval, since) for since in range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] + tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) # Combine tickers data: List = [] - for p, ticker in tickers: + for p, ticker_interval, ticker in tickers: if p == pair: 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]) logger.info("downloaded %s with length %s.", pair, len(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)) - asyncio.get_event_loop().run_until_complete( - self.async_get_candles_history(pair_list, ticker_interval)) + logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) - 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 = [] # Gather corotines to run - for pair in pairs: - if not (self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >= - arrow.utcnow().timestamp and pair in self._klines): - input_coroutines.append(self._async_get_candle_history(pair, tick_interval)) - else: - logger.debug("Using cached klines data for %s ...", pair) + for pair, ticker_interval in set(pair_list): + # Calculating ticker interval in second + interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60 - 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 - 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 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 - self._klines[pair] = parse_ticker_dataframe(ticks) + self._klines[(pair, tick_interval)] = parse_ticker_dataframe( + ticks, tick_interval, fill_missing=True) return tickers @retrier_async 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: # 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, since=since_ms) @@ -575,11 +589,14 @@ class Exchange(object): # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) # when GDAX returns a list of tickers DESC (newest first, oldest last) # Only sort if necessary to save computing time - if data and data[0][0] > data[-1][0]: - data = sorted(data, key=lambda x: x[0]) - - logger.debug("done fetching %s ...", pair) - return pair, data + try: + if data and data[0][0] > data[-1][0]: + data = sorted(data, key=lambda x: x[0]) + except IndexError: + logger.exception("Error loading %s. Result was %s.", 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: raise OperationalException( diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4febe9dd0..f67be724c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -15,6 +15,7 @@ from requests.exceptions import RequestException from freqtrade import (DependencyException, OperationalException, TemporaryError, __version__, constants, persistence) from freqtrade.data.converter import order_book_to_dataframe +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -34,11 +35,11 @@ class FreqtradeBot(object): This is from here the bot start its logic. """ - 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 - :param config: configuration dict, you can use the Configuration.get_config() - method to get the config dict. + Init all variables and objects the bot needs to work + :param config: configuration dict, you can use Configuration.get_config() + to get the config dict. """ logger.info( @@ -54,9 +55,15 @@ class FreqtradeBot(object): self.strategy: IStrategy = StrategyResolver(self.config).strategy self.rpc: RPCManager = RPCManager(self) - self.persistence = None self.exchange = Exchange(self.config) 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') self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist @@ -151,9 +158,6 @@ class FreqtradeBot(object): self.active_pair_whitelist = self.pairlists.whitelist # 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: self.edge.calculate() 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 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 - 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 for trade in trades: @@ -183,7 +191,7 @@ class FreqtradeBot(object): Trade.session.flush() 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) except OperationalException: tb = traceback.format_exc() @@ -196,19 +204,11 @@ class FreqtradeBot(object): self.state = State.STOPPED 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 - :param ticker: Ticker to use for getting Ask and Last 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', {}) if 'use_order_book' in config_bid_strategy and\ config_bid_strategy.get('use_order_book', False): @@ -218,15 +218,16 @@ class FreqtradeBot(object): logger.debug('order_book %s', order_book) # top 1 = index 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) - 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: 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 return used_rate @@ -259,9 +260,8 @@ class FreqtradeBot(object): # Check if stake_amount is fulfilled if avaliable_amount < stake_amount: raise DependencyException( - 'Available balance(%f %s) is lower than stake amount(%f %s)' % ( - avaliable_amount, self.config['stake_currency'], - stake_amount, self.config['stake_currency']) + f"Available balance({avaliable_amount} {self.config['stake_currency']}) is " + f"lower than stake amount({stake_amount} {self.config['stake_currency']})" ) return stake_amount @@ -290,7 +290,9 @@ class FreqtradeBot(object): if not min_stake_amounts: 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: amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% @@ -317,16 +319,16 @@ class FreqtradeBot(object): # running get_signal on historical data fetched 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: stake_amount = self._get_trade_stake_amount(_pair) if not stake_amount: return False - logger.info( - 'Buy signal found: about create a new trade with stake_amount: %f ...', - stake_amount - ) + logger.info(f"Buy signal found: about create a new trade with stake_amount: " + f"{stake_amount} ...") bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\ get('check_depth_of_market', {}) @@ -373,13 +375,13 @@ class FreqtradeBot(object): buy_limit_requested = price else: # 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) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( - f'Can\'t open a new trade for {pair_s}: stake amount' - f' is too small ({stake_amount} < {min_stake_amount})' + f'Can\'t open a new trade for {pair_s}: stake amount ' + f'is too small ({stake_amount} < {min_stake_amount})' ) return False @@ -497,7 +499,7 @@ class FreqtradeBot(object): trade.fee_open = 0 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) @@ -556,12 +558,12 @@ class FreqtradeBot(object): fee_abs += exectrade['fee']['cost'] 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") real_amount = amount - fee_abs if fee_abs != 0: - logger.info(f"""Applying fee on amount for {trade} \ -(from {order_amount} to {real_amount}) from Trades""") + logger.info(f"Applying fee on amount for {trade} " + f"(from {order_amount} to {real_amount}) from Trades") return real_amount def handle_trade(self, trade: Trade) -> bool: @@ -570,16 +572,16 @@ class FreqtradeBot(object): :return: True if trade has been sold, False otherwise """ 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) - sell_rate = self.exchange.get_ticker(trade.pair)['bid'] (buy, sell) = (False, False) experimental = self.config.get('experimental', {}) 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, - self.exchange.klines(trade.pair)) + (buy, sell) = self.strategy.get_signal( + trade.pair, self.strategy.ticker_interval, + self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) config_ask_strategy = self.config.get('ask_strategy', {}) 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): 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) - 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): return True - break + else: logger.debug('checking sell') + sell_rate = self.exchange.get_ticker(trade.pair)['bid'] if self.check_sell(trade, sell_rate, buy, sell): return True @@ -613,7 +612,7 @@ class FreqtradeBot(object): def handle_stoploss_on_exchange(self, trade: Trade) -> bool: """ 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. """ @@ -630,13 +629,14 @@ class FreqtradeBot(object): stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 + # 0.99 is arbitrary here. + limit_price = stop_price * 0.99 stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price )['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. # so we check if it is hit ... @@ -647,10 +647,38 @@ class FreqtradeBot(object): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(order) result = True - else: - result = False + elif self.config.get('trailing_stop', 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 + 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: if self.edge: stoploss = self.edge.stoploss(trade.pair) @@ -698,8 +726,15 @@ class FreqtradeBot(object): self.wallets.update() continue - # Check if trade is still actually open - if order['status'] == 'open': + # Handle cancelled on exchange + 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: self.handle_timedout_limit_buy(trade, order) self.wallets.update() @@ -707,24 +742,24 @@ class FreqtradeBot(object): self.handle_timedout_limit_sell(trade, order) self.wallets.update() - # FIX: 20180110, why is cancel.order unconditionally here, whereas - # it is conditionally called in the - # handle_timedout_limit_sell()? + def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None: + """Close trade in database and send message""" + 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: """Buy timeout - cancel order :return: True if order was fully cancelled """ - pair_s = trade.pair.replace('_', '/') self.exchange.cancel_order(trade.open_order_id, trade.pair) if order['remaining'] == order['amount']: # if trade is not partially completed, just delete the trade - Trade.session.delete(trade) - 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' - }) + self.handle_buy_order_full_cancel(trade, "cancelled due to timeout") return True # 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) self.rpc.send_msg({ '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 - # FIX: 20180110, should cancel_order() be cond. or unconditionally called? def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool: """ Sell timeout - cancel order and update trade :return: True if order was fully cancelled """ - pair_s = trade.pair.replace('_', '/') if order['remaining'] == order['amount']: # if trade is not partially completed, just cancel the trade - self.exchange.cancel_order(trade.open_order_id, trade.pair) + if order["status"] != "canceled": + reason = "due to timeout" + 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_profit = None trade.close_date = None @@ -756,9 +795,9 @@ class FreqtradeBot(object): trade.open_order_id = None self.rpc.send_msg({ '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 # TODO: figure out how to handle partially complete sell orders @@ -780,7 +819,7 @@ class FreqtradeBot(object): # we consider the sell price stop price if self.config.get('dry_run', False) and sell_type == 'stoploss' \ and self.strategy.order_types['stoploss_on_exchange']: - limit = trade.stop_loss + limit = trade.stop_loss # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: diff --git a/freqtrade/main.py b/freqtrade/main.py index 3ed478ec3..75b15915b 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -25,7 +25,7 @@ def main(sysargv: List[str]) -> None: """ arguments = Arguments( sysargv, - 'Simple High Frequency Trading Bot for crypto currencies' + 'Free, open source crypto trading bot' ) args = arguments.get_parsed_arg() @@ -39,13 +39,13 @@ def main(sysargv: List[str]) -> None: return_code = 1 try: # Load and validate configuration - config = Configuration(args).get_config() + config = Configuration(args, None).get_config() # Init the bot freqtrade = FreqtradeBot(config) state = None - while 1: + while True: state = freqtrade.worker(old_state=state) if state == State.RELOAD_CONF: freqtrade = reconfigure(freqtrade, args) @@ -76,7 +76,7 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: freqtrade.cleanup() # Create new instance - freqtrade = FreqtradeBot(Configuration(args).get_config()) + freqtrade = FreqtradeBot(Configuration(args, None).get_config()) freqtrade.rpc.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, 'status': 'config reloaded' diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 832437951..d03187d77 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -3,7 +3,6 @@ Various tool function for Freqtrade and scripts """ import gzip -import json import logging import re from datetime import datetime @@ -11,6 +10,7 @@ from typing import Dict import numpy as np from pandas import DataFrame +import rapidjson logger = logging.getLogger(__name__) @@ -38,12 +38,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: An numpy-array of datetimes :return: numpy-array of datetime """ - times = [] - dates = dates.astype(datetime) - for index in range(0, dates.size): - date = dates[index].to_pydatetime() - times.append(date) - return np.array(times) + return dates.dt.to_pydatetime() 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 :return: """ - print(f'dumping json to "{filename}"') + logger.info(f'dumping json to "{filename}"') if is_zip: if not filename.endswith('.gz'): filename = filename + '.gz' 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: 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: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cc05c5de8..a8f4e530a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -import freqtrade.optimize as optimize +from freqtrade import optimize from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -22,6 +22,7 @@ from freqtrade.data import history from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType, IStrategy logger = logging.getLogger(__name__) @@ -100,11 +101,13 @@ class Backtesting(object): :return: pretty printed table with tabulate as str """ 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 = [] 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: result = results[results.pair == pair] 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.sum() * 100.0, result.profit_abs.sum(), + result.profit_percent.sum() * 100.0 / max_open_trades, str(timedelta( minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', len(result[result.profit_abs > 0]), @@ -129,12 +133,15 @@ class Backtesting(object): results.profit_percent.mean() * 100.0, results.profit_percent.sum() * 100.0, results.profit_abs.sum(), + results.profit_percent.sum() * 100.0 / max_open_trades, str(timedelta( 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]) ]) - 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: """ @@ -151,11 +158,13 @@ class Backtesting(object): Generate summary table per strategy """ 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 = [] 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(): tabular_data.append([ strategy, @@ -163,12 +172,15 @@ class Backtesting(object): results.profit_percent.mean() * 100.0, results.profit_percent.sum() * 100.0, results.profit_abs.sum(), + results.profit_percent.sum() * 100.0 / max_open_trades, str(timedelta( 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]) ]) - 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, strategyname: Optional[str] = None) -> None: @@ -219,7 +231,8 @@ class Backtesting(object): # Set close_rate to stoploss closerate = trade.stop_loss 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, self.strategy.minimal_roi.keys()))) roi = self.strategy.minimal_roi[roi_entry] @@ -362,8 +375,9 @@ class Backtesting(object): if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') - self.exchange.refresh_tickers(pairs, self.ticker_interval) - data = self.exchange._klines + self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs]) + data = {key[0]: value for key, value in self.exchange._klines.items()} + else: 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()) self._set_strategy(strat) - # need to reprocess data every time to populate signals - preprocessed = self.strategy.tickerdata_to_dataframe(data) - - min_date, max_date = optimize.get_timeframe(preprocessed) - # Validate dataframe for missing values - optimize.validate_backtest_data(preprocessed, min_date, max_date, + min_date, max_date = optimize.get_timeframe(data) + # Validate dataframe for missing values (mainly at start and end, as fillup is called) + optimize.validate_backtest_data(data, min_date, max_date, constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]) logger.info( 'Measuring data from %s up to %s (%s days)..', @@ -406,6 +417,8 @@ class Backtesting(object): max_date.isoformat(), (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 all_results[self.strategy.get_strategy_name()] = self.backtest( @@ -426,18 +439,18 @@ class Backtesting(object): strategy if len(self.strategylist) > 1 else None) print(f"Result for strategy {strategy}") - print(' BACKTESTING REPORT '.center(119, '=')) + print(' BACKTESTING REPORT '.center(133, '=')) 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(' 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() if len(all_results) > 1: # Print Strategy summary table - print(' Strategy Summary '.center(119, '=')) + print(' Strategy Summary '.center(133, '=')) print(self._generate_text_table_strategy(all_results)) 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() :return: Configuration """ - configuration = Configuration(args) + configuration = Configuration(args, RunMode.BACKTEST) config = configuration.get_config() # Ensure we do not use Exchange credentials diff --git a/freqtrade/optimize/default_hyperopt.py b/freqtrade/optimize/default_hyperopt.py index 6139f8140..721848d2e 100644 --- a/freqtrade/optimize/default_hyperopt.py +++ b/freqtrade/optimize/default_hyperopt.py @@ -33,6 +33,7 @@ class DefaultHyperOpts(IHyperOpt): # Bollinger bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] dataframe['sar'] = ta.SAR(dataframe) return dataframe @@ -57,16 +58,17 @@ class DefaultHyperOpts(IHyperOpt): conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] - )) + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -93,6 +95,67 @@ class DefaultHyperOpts(IHyperOpt): 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 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.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 diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a98f0c877..9b628cf2e 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -9,10 +9,11 @@ from typing import Dict, Any from tabulate import tabulate from freqtrade.edge import Edge -from freqtrade.configuration import Configuration from freqtrade.arguments import Arguments +from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -67,7 +68,9 @@ class EdgeCli(object): 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: self.edge.calculate() @@ -81,7 +84,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]: :param args: Cli args from Arguments() :return: Configuration """ - configuration = Configuration(args) + configuration = Configuration(args, RunMode.EDGECLI) config = configuration.get_config() # Ensure we do not use Exchange credentials diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 2d08fec81..f6d39f11c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -5,17 +5,18 @@ This module contains the hyperopt logic """ import logging -from argparse import Namespace +import multiprocessing import os import sys -from pathlib import Path +from argparse import Namespace from math import exp -import multiprocessing from operator import itemgetter +from pathlib import Path +from pprint import pprint from typing import Any, Dict, List -from pandas import DataFrame from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects +from pandas import DataFrame from skopt import Optimizer from skopt.space import Dimension @@ -24,9 +25,9 @@ from freqtrade.configuration import Configuration from freqtrade.data.history import load_data from freqtrade.optimize import get_timeframe from freqtrade.optimize.backtesting import Backtesting +from freqtrade.state import RunMode from freqtrade.resolvers import HyperOptResolver - logger = logging.getLogger(__name__) 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')) best_result = results[0] logger.info( - 'Best result:\n%s\nwith values:\n%s', - best_result['result'], - best_result['params'] + 'Best result:\n%s\nwith values:\n', + best_result['result'] ) + pprint(best_result['params'], indent=4) if 'roi_t1' in best_result['params']: - logger.info('ROI table:\n%s', - self.custom_hyperopt.generate_roi_table(best_result['params'])) + logger.info('ROI table:') + pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) def log_results(self, results) -> None: """ @@ -151,6 +152,12 @@ class Hyperopt(Backtesting): spaces: List[Dimension] = [] if self.has_space('buy'): 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'): spaces += self.custom_hyperopt.roi_space() if self.has_space('stoploss'): @@ -164,6 +171,13 @@ class Hyperopt(Backtesting): if self.has_space('buy'): 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'): self.strategy.stoploss = params['stoploss'] @@ -247,7 +261,7 @@ class Hyperopt(Backtesting): timerange=timerange ) - if self.has_space('buy'): + if self.has_space('buy') or self.has_space('sell'): self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) @@ -293,7 +307,7 @@ def start(args: Namespace) -> None: # Initialize configuration # Monkey patch the configuration with hyperopt_conf.py - configuration = Configuration(args) + configuration = Configuration(args, RunMode.HYPEROPT) logger.info('Starting freqtrade in Hyperopt mode') config = configuration.load_config() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index d42206658..622de3015 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -37,6 +37,13 @@ class IHyperOpt(ABC): Create a buy strategy generator """ + @staticmethod + @abstractmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Create a sell strategy generator + """ + @staticmethod @abstractmethod def indicator_space() -> List[Dimension]: @@ -44,6 +51,13 @@ class IHyperOpt(ABC): Create an indicator space """ + @staticmethod + @abstractmethod + def sell_indicator_space() -> List[Dimension]: + """ + Create a sell indicator space + """ + @staticmethod @abstractmethod def generate_roi_table(params: Dict) -> Dict[int, float]: diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a14d22b98..f9b34fc64 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -83,7 +83,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # 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}') 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') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') 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') sell_reason = get_column_def(cols, 'sell_reason', '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, open_rate_requested, close_rate, close_rate_requested, close_profit, 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 ) select id, lower(exchange), @@ -127,9 +129,9 @@ def check_migrate(engine) -> None: {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, - {stoploss_order_id} stoploss_order_id, {max_rate} max_rate, - {sell_reason} sell_reason, {strategy} strategy, - {ticker_interval} ticker_interval + {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, + {max_rate} max_rate, {sell_reason} sell_reason, + {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -185,6 +187,8 @@ class Trade(_DECL_BASE): initial_stop_loss = Column(Float, nullable=True, default=0.0) # stoploss order id which is on exchange 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 max_rate = Column(Float, nullable=True, default=0.0) sell_reason = Column(String, nullable=True) @@ -218,11 +222,13 @@ class Trade(_DECL_BASE): logger.debug("assigning new stop loss") self.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 else: if new_loss > self.stop_loss: # stop losses only walk up, never down! self.stop_loss = new_loss + self.stoploss_last_update = datetime.utcnow() logger.debug("adjusted stop loss") else: logger.debug("keeping current stop loss") diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index eb91c0e89..6bf7fa17d 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -32,6 +32,13 @@ class HyperOptResolver(IResolver): hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT 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( self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: """ diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index c1d967383..01467b0a1 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -3,11 +3,11 @@ """ This module load custom strategies """ -import inspect import logging import tempfile from base64 import urlsafe_b64decode from collections import OrderedDict +from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional @@ -39,59 +39,67 @@ class StrategyResolver(IResolver): config=config, extra_dir=config.get('strategy_path')) + # make sure experimental dict is available + if 'experimental' not in config: + config['experimental'] = {} + # Set attributes # Check if we need to override configuration - if 'minimal_roi' in config: - self.strategy.minimal_roi = config['minimal_roi'] - logger.info("Override strategy 'minimal_roi' with value in config file: %s.", - config['minimal_roi']) - else: - config['minimal_roi'] = self.strategy.minimal_roi + # (Attribute name, default, experimental) + attributes = [("minimal_roi", None, False), + ("ticker_interval", None, False), + ("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: + self._override_attribute_helper(config, attribute, default) - if 'stoploss' in config: - self.strategy.stoploss = config['stoploss'] - logger.info( - "Override strategy 'stoploss' with value in config file: %s.", config['stoploss'] - ) - else: - config['stoploss'] = self.strategy.stoploss + # Loop this list again to have output combined + for attribute, _, exp in attributes: + if exp and attribute in config['experimental']: + logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute]) + elif attribute in config: + logger.info("Strategy using %s: %s", attribute, config[attribute]) - if 'ticker_interval' in config: - self.strategy.ticker_interval = config['ticker_interval'] - logger.info( - "Override strategy 'ticker_interval' with value in config file: %s.", - config['ticker_interval'] - ) - else: - config['ticker_interval'] = self.strategy.ticker_interval + # 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) - if 'process_only_new_candles' in config: - 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 + self._strategy_sanity_validations() - if 'order_types' in config: - self.strategy.order_types = config['order_types'] - logger.info( - "Override strategy 'order_types' with value in config file: %s.", - config['order_types'] - ) - else: - config['order_types'] = self.strategy.order_types - - if 'order_time_in_force' in config: - self.strategy.order_time_in_force = config['order_time_in_force'] - logger.info( - "Override strategy 'order_time_in_force' with value in config file: %s.", - config['order_time_in_force'] - ) - else: - config['order_time_in_force'] = self.strategy.order_time_in_force + def _override_attribute_helper(self, config, attribute: str, default): + """ + Override attributes in the strategy. + Prevalence: + - Configuration + - Strategy + - default (if not None) + """ + if attribute in config: + setattr(self.strategy, attribute, config[attribute]) + logger.info("Override strategy '%s' with value in config file: %s.", + attribute, config[attribute]) + elif hasattr(self.strategy, attribute): + config[attribute] = getattr(self.strategy, attribute) + # Explicitly check for None here as other "falsy" values are possible + elif default is not None: + 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): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") @@ -100,12 +108,6 @@ class StrategyResolver(IResolver): raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " 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( self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ @@ -149,11 +151,9 @@ class StrategyResolver(IResolver): if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path) strategy._populate_fun_len = len( - inspect.getfullargspec(strategy.populate_indicators).args) - strategy._buy_fun_len = len( - inspect.getfullargspec(strategy.populate_buy_trend).args) - strategy._sell_fun_len = len( - inspect.getfullargspec(strategy.populate_sell_trend).args) + getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) return import_strategy(strategy, config=config) except FileNotFoundError: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index be2498d78..3ce7c9167 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -246,14 +246,14 @@ class Telegram(RPC): stake_cur, fiat_disp_cur ) - stats = tabulate(stats, - headers=[ - 'Day', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}' - ], - tablefmt='simple') - message = f'Daily Profit over the last {timescale} days:\n
{stats}
' + stats_tab = tabulate(stats, + headers=[ + 'Day', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}' + ], + tablefmt='simple') + message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) diff --git a/freqtrade/state.py b/freqtrade/state.py index 42bfb6e41..b69c26cb5 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -3,13 +3,26 @@ """ Bot state constant """ -import enum +from enum import Enum -class State(enum.Enum): +class State(Enum): """ Bot application states """ - RUNNING = 0 - STOPPED = 1 - RELOAD_CONF = 2 + RUNNING = 1 + STOPPED = 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 diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index 085a383db..5c7d50a65 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -42,6 +42,19 @@ class DefaultStrategy(IStrategy): 'sell': 'gtc', } + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a5c10b58c..1d6147357 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,9 @@ import arrow from pandas import DataFrame from freqtrade import constants +from freqtrade.data.dataprovider import DataProvider from freqtrade.persistence import Trade +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -67,6 +69,11 @@ class IStrategy(ABC): # associated stoploss stoploss: float + # trailing stoploss + trailing_stop: bool = False + trailing_stop_positive: float + trailing_stop_positive_offset: float + # associated ticker interval ticker_interval: str @@ -75,7 +82,8 @@ class IStrategy(ABC): 'buy': 'limit', 'sell': 'limit', 'stoploss': 'limit', - 'stoploss_on_exchange': False + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_interval': 60, } # Optional time in force @@ -87,12 +95,16 @@ class IStrategy(ABC): # run "populate_indicators" only for new candle process_only_new_candles: bool = False - # Dict to determine if analysis is necessary - _last_candle_seen_per_pair: Dict[str, datetime] = {} + # Class level variables (intentional) containing + # 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: 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 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -121,6 +133,19 @@ class IStrategy(ABC): :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: """ Returns strategy class name @@ -141,19 +166,19 @@ class IStrategy(ABC): if (not self.process_only_new_candles or self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): # 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_buy(dataframe, metadata) dataframe = self.advise_sell(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] else: - logging.debug("Skippinig TA Analysis for already analyzed candle") + logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 dataframe['sell'] = 0 # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) - logging.debug("Loop Analysis Launched") + logger.debug("Loop Analysis Launched") return dataframe @@ -228,12 +253,9 @@ class IStrategy(ABC): current_rate = low or 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, - current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + current_time=date, current_profit=current_profit, + force_stoploss=force_stoploss) if stoplossflag.sell_flag: return stoplossflag @@ -271,14 +293,16 @@ class IStrategy(ABC): """ trailing_stop = self.config.get('trailing_stop', False) - trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss else self.stoploss, initial=True) - # evaluate if the stoploss was hit - if self.stoploss is not None and trade.stop_loss >= current_rate: + # evaluate if the stoploss was hit if stoploss is not on exchange + 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 - 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 logger.debug( 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 # and if profit is positive - stop_loss_value = self.stoploss - sl_offset = self.config.get('trailing_stop_positive_offset', 0.0) + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + + sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 if 'trailing_stop_positive' in self.config and current_profit > sl_offset: @@ -313,17 +338,18 @@ class IStrategy(ABC): 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 - sell + sell. Requires current_profit to be in percent!! :return True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - for duration, threshold in self.minimal_roi.items(): - if time_diff <= duration: - return False - if current_profit > threshold: - return True + trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 + + # Get highest entry in ROI dict where key >= trade-duration + roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) + threshold = self.minimal_roi[roi_entry] + if current_profit > threshold: + return True return False diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0a2633c7c..809dc12e0 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring import json import logging +import re from datetime import datetime from functools import reduce from typing import Dict, Optional @@ -27,6 +28,12 @@ def log_has(line, logs): 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: mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -542,7 +549,7 @@ def ticker_history_list(): @pytest.fixture def ticker_history(ticker_history_list): - return parse_ticker_dataframe(ticker_history_list) + return parse_ticker_dataframe(ticker_history_list, "5m", True) @pytest.fixture @@ -724,7 +731,7 @@ def tickers(): @pytest.fixture def result(): 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: # Create an fixture/function diff --git a/freqtrade/tests/data/test_converter.py b/freqtrade/tests/data/test_converter.py index 54f32b341..46d564003 100644 --- a/freqtrade/tests/data/test_converter.py +++ b/freqtrade/tests/data/test_converter.py @@ -1,25 +1,99 @@ # pragma pylint: disable=missing-docstring, C0103 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 -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): - assert result.columns.tolist() == \ - ['date', 'open', 'high', 'low', 'close', 'volume'] + assert result.columns.tolist() == ['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'] caplog.set_level(logging.DEBUG) # 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 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) diff --git a/freqtrade/tests/data/test_dataprovider.py b/freqtrade/tests/data/test_dataprovider.py new file mode 100644 index 000000000..b17bba273 --- /dev/null +++ b/freqtrade/tests/data/test_dataprovider.py @@ -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 diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 8923a60b6..bc859b325 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -450,7 +450,7 @@ def test_trim_tickerlist() -> None: 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', 'test_{id}.json'.format(id=str(uuid.uuid4()))) data = {'bar': 'foo'} diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index 4fbe9c494..c1c1b49cd 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -281,8 +281,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals 123.45 ] for x in range(0, 500)] - pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC), - 'LTC/BTC': parse_ticker_dataframe(LTCBTC)} + pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True), + 'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', fill_missing=True)} return pairdata diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 29154bc39..b384035b0 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -765,7 +765,7 @@ def test_get_history(default_conf, mocker, caplog): pair = 'ETH/BTC' 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) # 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 -def test_refresh_tickers(mocker, default_conf, caplog) -> None: +def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: tick = [ [ (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._api_async.fetch_ohlcv = get_mock_coro(tick) - pairs = ['IOTA/ETH', 'XRP/ETH'] + pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] # empty dicts 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._api_async.fetch_ohlcv.call_count == 2 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) # 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 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 @@ -850,11 +851,12 @@ async def test__async_get_candle_history(default_conf, mocker, caplog): pair = 'ETH/BTC' res = await exchange._async_get_candle_history(pair, "5m") assert type(res) is tuple - assert len(res) == 2 + assert len(res) == 3 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 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) 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' res = await exchange._async_get_candle_history(pair, "5m") assert type(res) is tuple - assert len(res) == 2 + assert len(res) == 3 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 -@pytest.mark.asyncio -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) - ] - ] +def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): - async def mock_get_candle_hist(pair, tick_interval, since_ms=None): - return (pair, tick) + async def mock_get_candle_hist(pair, *args, **kwargs): + if pair == 'ETH/BTC': + return [[]] + else: + raise TypeError() 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 len(res) == 2 - assert type(res[0]) is tuple - assert res[0][0] == pairs[0] - assert res[0][1] == tick - assert res[1][0] == pairs[1] - assert res[1][1] == tick - assert exchange._async_get_candle_history.call_count == 2 + # Test that each is in list at least once as order is not guaranteed + assert type(res[0]) is tuple or type(res[1]) is tuple + assert type(res[0]) is TypeError or type(res[1]) is TypeError + assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples) + assert log_has("Async code raised an exception: TypeError", caplog.record_tuples) 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 res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) assert res[0] == 'ETH/BTC' - ticks = res[1] + ticks = res[2] assert sort_mock.call_count == 1 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 res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) 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 assert sort_mock.call_count == 0 assert ticks[0][0] == 1527827700000 diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 8f7c80523..e69b1374e 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -18,6 +18,7 @@ from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.optimize import get_timeframe from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) +from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.interface import SellType from freqtrade.tests.conftest import log_has, patch_exchange @@ -75,7 +76,7 @@ def load_data_test(what): pair[x][5] # Keep old volume ] 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: @@ -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, timerange=None, exchange=None): 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 @@ -200,12 +201,15 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'timerange' 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( read_data=json.dumps(default_conf) )) + mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -229,6 +233,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config + assert config['runmode'] == RunMode.BACKTEST + assert log_has( 'Using data folder: {} ...'.format(config['datadir']), caplog.record_tuples @@ -322,15 +328,15 @@ def test_backtesting_init(mocker, default_conf) -> None: 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) timerange = TimeRange(None, 'line', 0, -100) 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) 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 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): patch_exchange(mocker) + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) results = pd.DataFrame( @@ -355,13 +362,13 @@ def test_generate_text_table(default_conf, mocker): result_str = ( '| 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' - '| ETH/BTC | 2 | 15.00 | 30.00 | ' - '0.60000000 | 0:20:00 | 2 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | ' - '0.60000000 | 0:20:00 | 2 | 0 |' + '-----------------:|---------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' ) 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 """ patch_exchange(mocker) + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) results = {} results['ETH/BTC'] = pd.DataFrame( @@ -424,13 +432,13 @@ def test_generate_text_table_strategyn(default_conf, mocker): result_str = ( '| 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 ' - '| 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 ' - '| 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)) 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.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) mocker.patch.multiple( '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.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) mocker.patch.multiple( 'freqtrade.optimize.backtesting.Backtesting', @@ -526,13 +534,14 @@ def test_backtest(default_conf, fee, mocker) -> None: {'pair': [pair, pair], 'profit_percent': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime, - Arrow(2018, 1, 30, 3, 30, 0).datetime], - 'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime, - Arrow(2018, 1, 30, 4, 15, 0).datetime], + 'open_time': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, + Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True + ), + '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], - 'close_index': [125, 193], - 'trade_duration': [235, 45], + 'close_index': [125, 192], + 'trade_duration': [235, 40], 'open_at_end': [False, False], 'open_rate': [0.104445, 0.10302485], '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: 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!! default_conf['experimental'] = {"use_sell_signal": True} @@ -654,8 +663,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): def test_backtest_multi_pair(default_conf, fee, mocker): def evaluate_result_multi(results, freq, max_open_trades): - # Find overlapping trades by expanding each trade once per period - # and then counting overlaps + # Find overlapping trades by expanding each trade once per period + # and then counting overlaps dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq)) for row in results[['open_time', 'close_time']].iterrows()] deltas = [len(x) for x in dates] diff --git a/freqtrade/tests/optimize/test_edge_cli.py b/freqtrade/tests/optimize/test_edge_cli.py index 0d0f64e0c..a58620139 100644 --- a/freqtrade/tests/optimize/test_edge_cli.py +++ b/freqtrade/tests/optimize/test_edge_cli.py @@ -7,6 +7,7 @@ from typing import List from freqtrade.edge import PairInfo from freqtrade.arguments import Arguments from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start) +from freqtrade.state import RunMode 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)) + assert config['runmode'] == RunMode.EDGECLI + assert 'max_open_trades' in config assert 'stake_currency' 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 -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( read_data=json.dumps(edge_conf) )) + mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) args = [ '--config', 'config.json', @@ -69,6 +73,7 @@ def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None: assert 'exchange' in config assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config + assert config['runmode'] == RunMode.EDGECLI assert log_has( 'Using data folder: {} ...'.format(config['datadir']), caplog.record_tuples diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 5a40441bb..20baee99e 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -9,7 +9,8 @@ import pytest from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file 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.optimize.test_backtesting import get_args @@ -38,6 +39,28 @@ def create_trials(mocker, hyperopt) -> None: 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: start_mock = MagicMock() mocker.patch( @@ -201,7 +224,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: hyperopt.start() 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 @@ -243,7 +266,7 @@ def test_has_space(hyperopt): def test_populate_indicators(hyperopt) -> None: 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) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -256,7 +279,7 @@ def test_populate_indicators(hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None: 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) dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) @@ -312,6 +335,15 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'mfi-enabled': False, 'rsi-enabled': False, '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_t2': 30.0, 'roi_t3': 20.0, diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 02d0e9c36..99cd24c26 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -30,7 +30,8 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None: history.load_data( datadir=None, ticker_interval='1m', - pairs=['UNITTEST/BTC'] + pairs=['UNITTEST/BTC'], + fill_up_missing=False ) ) min_date, max_date = optimize.get_timeframe(data) diff --git a/freqtrade/tests/rpc/test_fiat_convert.py b/freqtrade/tests/rpc/test_fiat_convert.py index 7d857d2f1..fbc942432 100644 --- a/freqtrade/tests/rpc/test_fiat_convert.py +++ b/freqtrade/tests/rpc/test_fiat_convert.py @@ -117,7 +117,7 @@ def test_fiat_convert_get_price(mocker): assert fiat_convert._pairs[0].crypto_symbol == 'BTC' assert fiat_convert._pairs[0].fiat_symbol == 'USD' 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 # Verify the cached is used diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index 45ed54c4d..be514f2d1 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -10,7 +10,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy @pytest.fixture def result(): 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(): diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 22ba9a2b6..d6ef0c8e7 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -111,32 +111,78 @@ def test_tickerdata_to_dataframe(default_conf) -> None: timerange = TimeRange(None, 'line', 0, -100) 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) - 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: - strategy = DefaultStrategy(default_conf) - strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01} - 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.01, arrow.utcnow().shift(minutes=-55).datetime) - assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime) + # 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.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.04, arrow.utcnow().shift(minutes=-39).datetime) - assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).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=-56).datetime) - assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime) - assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).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 not strategy.min_roi_reached(trade, -0.01, 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: @@ -158,7 +204,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: assert buy_mock.call_count == 1 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.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 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) @@ -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 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.clear() @@ -211,5 +257,5 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: assert ret['buy'].sum() == 0 assert ret['sell'].sum() == 0 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) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 08d95fc5d..602ea5dbe 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -150,6 +150,45 @@ def test_strategy_override_stoploss(caplog): ) 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): 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 ('freqtrade.resolvers.strategy_resolver', logging.INFO, - "Override process_only_new_candles 'process_only_new_candles' " - "with value in config file: True." + "Override strategy 'process_only_new_candles' with value in config file: True." ) in caplog.record_tuples @@ -256,6 +294,62 @@ def test_strategy_override_order_tif(caplog): 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): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', @@ -270,7 +364,7 @@ def test_deprecate_populate_indicators(result): in str(w[-1].message) with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. + # Cause all warnings to always be triggered. warnings.simplefilter("always") resolver.strategy.advise_buy(indicators, 'ETH/BTC') assert len(w) == 1 diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index d28ab30af..042d43ed2 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -47,7 +47,7 @@ def test_scripts_options() -> None: arguments = Arguments(['-p', 'ETH/BTC'], '') arguments.scripts_options() args = arguments.get_parsed_arg() - assert args.pair == 'ETH/BTC' + assert args.pairs == 'ETH/BTC' def test_parse_args_version() -> None: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index be381770b..67445238b 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -13,6 +13,7 @@ from freqtrade import OperationalException from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration, set_loggers from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL +from freqtrade.state import RunMode 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() configuration = Configuration(args) validated_conf = configuration.load_config() - print(validated_conf) assert validated_conf['max_open_trades'] > 999999999 assert validated_conf['max_open_trades'] == float('inf') 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: @@ -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('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["dry_run"] = False del conf["db_url"] @@ -141,8 +180,10 @@ def test_load_config_with_params(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() 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["dry_run"] = True 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( read_data=json.dumps(default_conf) )) + mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) arglist = [ '--config', 'config.json', @@ -328,8 +370,9 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non args = Arguments(arglist, '').get_parsed_arg() - configuration = Configuration(args) + configuration = Configuration(args, RunMode.BACKTEST) config = configuration.get_config() + assert config['runmode'] == RunMode.BACKTEST assert 'max_open_trades' in config assert 'stake_currency' 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() - configuration = Configuration(args) + configuration = Configuration(args, RunMode.HYPEROPT) config = configuration.get_config() assert 'epochs' in config @@ -385,6 +428,8 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: assert 'spaces' in config assert config['spaces'] == ['all'] 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: @@ -487,3 +532,13 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_validate_default_conf(default_conf) -> None: 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) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 0b6a14112..a0ac6ee99 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -18,7 +18,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State 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 @@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :return: None """ 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: @@ -691,7 +691,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, assert trade.amount == 90.99181073703367 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 ) @@ -807,25 +807,61 @@ def test_process_trade_no_whitelist_pair( 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: default_conf['bid_strategy']['ask_last_balance'] = 0.0 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: default_conf['bid_strategy']['ask_last_balance'] = 1.0 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: default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - - assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5 + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + 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: @@ -1014,6 +1050,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 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: freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -1082,7 +1323,7 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, side_effect=OperationalException() ) 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 mocker.patch( @@ -1318,6 +1559,47 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe 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, fee, mocker) -> None: 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 +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, mocker) -> None: rpc_mock = patch_RPCManager(mocker) @@ -1508,7 +1829,8 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: trade = MagicMock() order = {'remaining': 1, - 'amount': 1} + 'amount': 1, + 'status': "open"} assert freqtrade.handle_timedout_limit_sell(trade, order) assert cancel_order_mock.call_count == 1 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.update(limit_buy_order) + trade.max_rate = trade.open_rate * 1.003 caplog.set_level(logging.DEBUG) # Sell as trailing-stop is reached 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 """ patch_exchange(mocker) + ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', 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['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 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: @@ -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) """ patch_exchange(mocker) + ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', 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['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 freqtrade = FreqtradeBot(default_conf) - assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.042, 'last': 0.046}) == 0.042 - - -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 + # 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_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None: diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 017bf372f..2da6b8718 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, - file_dump_json, format_ms_time, shorten_date) -from freqtrade.data.history import load_tickerdata_file + file_dump_json, file_load_json, format_ms_time, shorten_date) +from freqtrade.data.history import load_tickerdata_file, make_testdata_path from freqtrade.strategy.default_strategy import DefaultStrategy @@ -17,7 +17,7 @@ def test_shorten_date() -> None: 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']) 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: strategy = DefaultStrategy(default_conf) 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) dates = common_datearray(dataframes) assert dates.size == dataframes['UNITTEST/BTC']['date'].size 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: 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]) assert file_open.call_count == 1 assert json_dump.call_count == 1 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) assert file_open.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: # Date 2018-04-10 18:02:01 date_in_epoch_ms = 1523383321000 diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index e64a08262..be6efc2ff 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.strategy is None assert trade.ticker_interval 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_bak2", caplog.record_tuples) assert log_has("Running database migration - backup available as trades_bak2", diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index e68932998..3866d36c1 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -236,7 +236,7 @@ def crossed(series1, series2, direction=None): if direction is None: return above or below - return above if direction is "above" else below + return above if direction == "above" else below def crossed_above(series1, series2): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 59d8fa3da..1f1d2c511 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,12 +2,12 @@ """ Wallet """ import logging from typing import Dict, Any, NamedTuple -from collections import namedtuple from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) +# wallet data structure class Wallet(NamedTuple): exchange: str currency: str @@ -18,12 +18,6 @@ class Wallet(NamedTuple): class Wallets(object): - # wallet data structure - wallet = namedtuple( - 'wallet', - ['exchange', 'currency', 'free', 'used', 'total'] - ) - def __init__(self, exchange: Exchange) -> None: self.exchange = exchange self.wallets: Dict[str, Any] = {} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..36428d1c1 --- /dev/null +++ b/mkdocs.yml @@ -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 diff --git a/requirements-dev.txt b/requirements-dev.txt index 4a7416ca1..34d59d802 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,12 @@ # Include all requirements to run the bot. -r requirements.txt -flake8==3.6.0 -pytest==4.0.2 -pytest-mock==1.10.0 -pytest-asyncio==0.9.0 -pytest-cov==2.6.0 +flake8==3.7.6 +flake8-type-annotations==0.1.0 +flake8-tidy-imports==2.0.0 +pytest==4.3.0 +pytest-mock==1.10.1 +pytest-asyncio==0.10.0 +pytest-cov==2.6.1 +coveralls==1.6.0 +mypy==0.670 diff --git a/requirements-plot.txt b/requirements-plot.txt new file mode 100644 index 000000000..c01ea6a60 --- /dev/null +++ b/requirements-plot.txt @@ -0,0 +1,5 @@ +# Include all requirements to run the bot. +-r requirements.txt + +plotly==3.6.1 + diff --git a/requirements.txt b/requirements.txt index aa1147f9a..b4dd302e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,19 @@ -ccxt==1.18.71 -SQLAlchemy==1.2.15 +ccxt==1.18.270 +SQLAlchemy==1.2.18 python-telegram-bot==11.1.0 -arrow==0.12.1 -cachetools==3.0.0 +arrow==0.13.1 +cachetools==3.1.0 requests==2.21.0 urllib3==1.24.1 -wrapt==1.10.11 -pandas==0.23.4 +wrapt==1.11.1 +numpy==1.16.1 +pandas==0.24.1 scikit-learn==0.20.2 -joblib==0.13.0 -scipy==1.2.0 +joblib==0.13.2 +scipy==1.2.1 jsonschema==2.6.0 -numpy==1.15.4 TA-Lib==0.4.17 -tabulate==0.8.2 +tabulate==0.8.3 coinmarketcap==5.0.3 # Required for hyperopt @@ -23,4 +23,4 @@ scikit-optimize==0.5.2 py_find_1st==1.1.3 #Load ticker files 30% faster -ujson==1.35 +python-rapidjson==0.7.0 diff --git a/scripts/download_backtest_data.py b/scripts/download_backtest_data.py index c4a1b59c5..c8fd08747 100755 --- a/scripts/download_backtest_data.py +++ b/scripts/download_backtest_data.py @@ -31,6 +31,7 @@ if args.config: configuration = Configuration(args) config = configuration._load_config_file(args.config) + config['stake_currency'] = '' # Ensure we do not use Exchange credentials config['exchange']['key'] = '' config['exchange']['secret'] = '' diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index ae9cd7f1d..6b954ac4c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -1,18 +1,18 @@ #!/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: --p / --pair: pair to examine +-p / --pairs: pair(s) to examine Option but recommended -s / --strategy: strategy to use 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. --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 @@ -21,8 +21,8 @@ Row 1: sma, ema3, ema5, ema10, ema50 Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk Example of usage: -> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3 ---indicators2 fastk,fastd +> python3 scripts/plot_dataframe.py --pairs BTC/EUR,XRP/BTC -d user_data/data/ + --indicators1 sma,ema3 --indicators2 fastk,fastd """ import json import logging @@ -65,7 +65,8 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram t.open_date.replace(tzinfo=timeZone), t.close_date.replace(tzinfo=timeZone) if t.close_date else None, 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()], columns=columns) @@ -74,52 +75,66 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram # must align with columns in backtest.py columns = ["pair", "profit", "opents", "closets", "index", "duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] - with file.open() as f: - data = json.load(f) - trades = pd.DataFrame(data, columns=columns) - trades = trades.loc[trades["pair"] == pair] - if timerange: - if timerange.starttype == 'date': - trades = trades.loc[trades["opents"] >= timerange.startts] - if timerange.stoptype == 'date': - trades = trades.loc[trades["opents"] <= timerange.stopts] + if file.exists(): + with file.open() as f: + data = json.load(f) + trades = pd.DataFrame(data, columns=columns) + trades = trades.loc[trades["pair"] == pair] + if timerange: + if timerange.starttype == 'date': + trades = trades.loc[trades["opents"] >= timerange.startts] + if timerange.stoptype == 'date': + trades = trades.loc[trades["opents"] <= timerange.stopts] + + trades['opents'] = pd.to_datetime( + trades['opents'], + unit='s', + utc=True, + infer_datetime_format=True) + trades['closets'] = pd.to_datetime( + trades['closets'], + unit='s', + utc=True, + infer_datetime_format=True) + else: + trades = pd.DataFrame([], columns=columns) - trades['opents'] = pd.to_datetime(trades['opents'], - unit='s', - utc=True, - infer_datetime_format=True) - trades['closets'] = pd.to_datetime(trades['closets'], - unit='s', - utc=True, - infer_datetime_format=True) 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 """ + 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 # Load the configuration _CONF.update(setup_configuration(args)) - print(_CONF) - # Set the pair to audit - pair = args.pair - if pair is None: - logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC') + pairs = args.pairs.split(',') + if pairs is None: + logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC') 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 try: strategy = StrategyResolver(_CONF).strategy @@ -131,61 +146,84 @@ def plot_analyzed_dataframe(args: Namespace) -> None: ) exit() - # Set the ticker to use - tick_interval = strategy.ticker_interval + return [strategy, exchange, pairs] + + +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 = {} if args.live: - logger.info('Downloading pair.') - exchange.refresh_tickers([pair], tick_interval) - tickers[pair] = exchange.klines(pair) + logger.info('Downloading pairs.') + exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs]) + for pair in pairs: + tickers[pair] = exchange.klines((pair, tick_interval)) else: tickers = history.load_data( datadir=Path(_CONF.get("datadir")), - pairs=[pair], + pairs=pairs, ticker_interval=tick_interval, refresh_pairs=_CONF.get('refresh_pairs', False), timerange=timerange, exchange=Exchange(_CONF) ) - # No ticker found, or impossible to download - if tickers == {}: - exit() + # No ticker found, impossible to download, len mismatch + for pair, data in tickers.copy().items(): + 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) - dataframe = dataframes[pair] dataframe = strategy.advise_buy(dataframe, {'pair': pair}) dataframe = strategy.advise_sell(dataframe, {'pair': pair}) - if len(dataframe.index) > args.plot_limit: - logger.warning('Ticker contained more than %s candles as defined ' - 'with --plot-limit, clipping.', args.plot_limit) - dataframe = dataframe.tail(args.plot_limit) + return dataframe + +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']] - fig = generate_graph( - pair=pair, - trades=trades, - data=dataframe, - args=args - ) - - plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html'))) + return trades -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 :param pair: Pair to Display on the graph :param trades: All trades created :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 """ @@ -201,6 +239,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title='Volume') fig['layout']['yaxis3'].update(title='Other') + fig['layout']['xaxis']['rangeslider'].update(visible=False) # Common information 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_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(sells, 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) # 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 @@ -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 ' 'them with a coma. E.g: fastd,fastk (default: %(default)s)', type=str, - default='macd', + default='macd,macdsignal', dest='indicators2', ) arguments.parser.add_argument( @@ -366,15 +405,58 @@ def plot_parse_args(args: List[str]) -> Namespace: 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: """ This function will initiate the bot and start the trading loop. :return: None """ logger.info('Starting Plot Dataframe') - plot_analyzed_dataframe( + analyse_and_plot_pairs( plot_parse_args(sysargv) ) + exit() if __name__ == '__main__': diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index a1561bc89..e2f85932f 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -29,6 +29,7 @@ from freqtrade.configuration import Configuration from freqtrade import constants from freqtrade.data import history from freqtrade.resolvers import StrategyResolver +from freqtrade.state import RunMode import freqtrade.misc as misc @@ -82,7 +83,7 @@ def plot_profit(args: Namespace) -> None: # to match the tickerdata against the profits-results timerange = Arguments.parse_timerange(args.timerange) - config = Configuration(args).get_config() + config = Configuration(args, RunMode.OTHER).get_config() # Init strategy try: @@ -107,8 +108,8 @@ def plot_profit(args: Namespace) -> None: exit(1) # Take pairs from the cli otherwise switch to the pair in the config file - if args.pair: - filter_pairs = args.pair + if args.pairs: + filter_pairs = args.pairs filter_pairs = filter_pairs.split(',') else: filter_pairs = config['exchange']['pair_whitelist'] diff --git a/setup.py b/setup.py index b9e3620df..35fdb2938 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ from freqtrade import __version__ setup(name='freqtrade', version=__version__, - description='Simple High Frequency Trading Bot for crypto currencies', + description='Crypto Trading Bot', url='https://github.com/freqtrade/freqtrade', author='gcarq and contributors', author_email='michael.egger@tsn.at', @@ -38,7 +38,7 @@ setup(name='freqtrade', 'cachetools', 'coinmarketcap', 'scikit-optimize', - 'ujson', + 'python-rapidjson', 'py_find_1st' ], include_package_data=True, diff --git a/setup.sh b/setup.sh index b8e99e679..66d449037 100755 --- a/setup.sh +++ b/setup.sh @@ -17,23 +17,27 @@ function check_installed_python() { return 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 "Update your virtual env" + echo "Updating your virtual env" echo "-------------------------" source .env/bin/activate echo "pip3 install in-progress. Please wait..." - pip3 install --quiet --upgrade pip - pip3 install --quiet -r requirements.txt --upgrade - pip3 install --quiet -r requirements.txt + # Install numpy first to have py_find_1st install clean + pip3 install --upgrade pip numpy + 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]$ ]] then - pip3 install --quiet -r requirements-dev.txt --upgrade - pip3 install --quiet -r requirements-dev.txt + pip3 install --upgrade -r requirements-dev.txt else echo "Dev dependencies ignored." fi @@ -44,7 +48,13 @@ function updateenv () { } # 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 cd ta-lib sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h @@ -52,14 +62,15 @@ function install_talib () { make sudo make install cd .. && rm -rf ./ta-lib/ + cd .. } # Install bot MacOS -function install_macos () { +function install_macos() { if [ ! -x "$(command -v brew)" ] then echo "-------------------------" - echo "Install Brew" + echo "Installing Brew" echo "-------------------------" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi @@ -69,7 +80,7 @@ function install_macos () { } # Install bot Debian_ubuntu -function install_debian () { +function install_debian() { sudo add-apt-repository ppa:jonathonf/python-3.6 sudo apt-get update sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git @@ -77,15 +88,15 @@ function install_debian () { } # Upgrade the bot -function update () { +function update() { git pull updateenv } # Reset Develop or Master branch -function reset () { +function reset() { echo "----------------------------" - echo "Reset branch and virtual env" + echo "Reseting branch and virtual env" echo "----------------------------" if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] then @@ -127,11 +138,11 @@ function test_and_fix_python_on_mac() { fi } -function config_generator () { +function config_generator() { echo "Starting to generate config.json" echo - echo "General configuration" + echo "Generating General configuration" echo "-------------------------" default_max_trades=3 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} echo - echo "Exchange config generator" + echo "Generating exchange config " echo "------------------------" read -p "Exchange API key: " api_key read -p "Exchange API Secret: " api_secret echo - echo "Telegram config generator" + echo "Generating Telegram config" echo "-------------------------" read -p "Telegram Token: " token read -p "Telegram Chat_id: " chat_id @@ -173,14 +184,14 @@ function config_generator () { } -function config () { +function config() { echo "-------------------------" - echo "Config file generator" + echo "Generating config file" echo "-------------------------" if [ -f config.json ] 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]$ ]] then config_generator @@ -199,9 +210,9 @@ function config () { echo } -function install () { +function install() { echo "-------------------------" - echo "Install mandatory dependencies" + echo "Installing mandatory dependencies" echo "-------------------------" if [ "$(uname -s)" == "Darwin" ] @@ -222,21 +233,21 @@ function install () { reset config echo "-------------------------" - echo "Run the bot" + echo "Run the bot !" echo "-------------------------" echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'." } -function plot () { +function plot() { echo " ----------------------------------------- -Install dependencies for Plotting scripts +Installing dependencies for Plotting scripts ----------------------------------------- " pip install plotly --upgrade } -function help () { +function help() { echo "usage:" echo " -i,--install Install freqtrade from scratch" echo " -u,--update Command git pull to update." diff --git a/user_data/hyperopts/sample_hyperopt.py b/user_data/hyperopts/sample_hyperopt.py index f11236a82..54f65a7e6 100644 --- a/user_data/hyperopts/sample_hyperopt.py +++ b/user_data/hyperopts/sample_hyperopt.py @@ -42,6 +42,7 @@ class SampleHyperOpts(IHyperOpt): # Bollinger bands bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] dataframe['sar'] = ta.SAR(dataframe) return dataframe @@ -66,16 +67,17 @@ class SampleHyperOpts(IHyperOpt): conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] - )) + if 'trigger' in params: + if params['trigger'] == 'bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_above( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_above( + dataframe['close'], dataframe['sar'] + )) dataframe.loc[ reduce(lambda x, y: x & y, conditions), @@ -102,6 +104,67 @@ class SampleHyperOpts(IHyperOpt): 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 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.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 diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index f72677e3d..3cb78842f 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -42,12 +42,22 @@ class TestStrategy(IStrategy): # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.10 + # trailing stoploss + trailing_stop = False + trailing_stop_positive = 0.01 + trailing_stop_positive_offset = 0.0 # Disabled / not configured + # Optimal ticker interval for the strategy ticker_interval = '5m' # run "populate_indicators" only for new candle 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 order_types = { 'buy': 'limit', @@ -62,6 +72,19 @@ class TestStrategy(IStrategy): 'sell': 'gtc' } + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame @@ -243,7 +266,8 @@ class TestStrategy(IStrategy): ( (dataframe['adx'] > 30) & (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 @@ -260,7 +284,8 @@ class TestStrategy(IStrategy): ( (dataframe['adx'] > 70) & (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 return dataframe