mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 04:03:55 +00:00
commit
905beef8a3
|
@ -21,6 +21,7 @@ search: False
|
||||||
requirements:
|
requirements:
|
||||||
- requirements.txt
|
- requirements.txt
|
||||||
- requirements-dev.txt
|
- requirements-dev.txt
|
||||||
|
- requirements-plot.txt
|
||||||
|
|
||||||
|
|
||||||
# configure the branch prefix the bot is using
|
# configure the branch prefix the bot is using
|
||||||
|
|
8
.readthedocs.yml
Normal file
8
.readthedocs.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# .readthedocs.yml
|
||||||
|
|
||||||
|
build:
|
||||||
|
image: latest
|
||||||
|
|
||||||
|
python:
|
||||||
|
version: 3.6
|
||||||
|
setup_py_install: false
|
13
.travis.yml
13
.travis.yml
|
@ -1,7 +1,7 @@
|
||||||
sudo: true
|
sudo: true
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
dist: trusty
|
dist: xenial
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 3.6
|
- 3.6
|
||||||
|
@ -17,9 +17,9 @@ addons:
|
||||||
- libdw-dev
|
- libdw-dev
|
||||||
- binutils-dev
|
- binutils-dev
|
||||||
install:
|
install:
|
||||||
- ./build_helpers/install_ta-lib.sh
|
- cd build_helpers && ./install_ta-lib.sh; cd ..
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
|
- pip install --upgrade pytest-random-order
|
||||||
- pip install -r requirements-dev.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -27,7 +27,6 @@ jobs:
|
||||||
- stage: tests
|
- stage: tests
|
||||||
script:
|
script:
|
||||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
- coveralls
|
|
||||||
name: pytest
|
name: pytest
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
|
@ -48,11 +47,13 @@ jobs:
|
||||||
- build_helpers/publish_docker.sh
|
- build_helpers/publish_docker.sh
|
||||||
name: "Build and test and push docker image"
|
name: "Build and test and push docker image"
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- coveralls
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
slack:
|
slack:
|
||||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||||
cache:
|
cache:
|
||||||
|
pip: True
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.cache/pip
|
- /usr/local/lib
|
||||||
- ta-lib
|
|
||||||
|
|
|
@ -14,6 +14,10 @@ Few pointers for contributions:
|
||||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
||||||
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started.
|
||||||
|
|
||||||
## Before sending the PR:
|
## Before sending the PR:
|
||||||
|
|
||||||
### 1. Run unit tests
|
### 1. Run unit tests
|
||||||
|
@ -41,12 +45,6 @@ pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
|
||||||
|
|
||||||
### 2. Test if your code is PEP8 compliant
|
### 2. Test if your code is PEP8 compliant
|
||||||
|
|
||||||
#### Install packages
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip3.6 install flake8 coveralls
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Run Flake8
|
#### Run Flake8
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -60,22 +58,12 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using
|
||||||
|
|
||||||
### 3. Test if all type-hints are correct
|
### 3. Test if all type-hints are correct
|
||||||
|
|
||||||
#### Install packages
|
|
||||||
|
|
||||||
``` bash
|
|
||||||
pip3.6 install mypy
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Run mypy
|
#### Run mypy
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
mypy freqtrade
|
mypy freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started.
|
|
||||||
|
|
||||||
## (Core)-Committer Guide
|
## (Core)-Committer Guide
|
||||||
|
|
||||||
### Process: Pull Requests
|
### Process: Pull Requests
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.7.0-slim-stretch
|
FROM python:3.7.2-slim-stretch
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install curl build-essential \
|
&& apt-get -y install curl build-essential \
|
||||||
|
|
96
README.md
96
README.md
|
@ -1,10 +1,11 @@
|
||||||
# freqtrade
|
# Freqtrade
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
|
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
|
[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram.
|
Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning.
|
||||||
|
|
||||||
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
||||||
|
|
||||||
|
@ -27,48 +28,27 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
|
||||||
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||||
|
|
||||||
|
Please find the complete documentation on our [website](https://www.freqtrade.io).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux
|
- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux.
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
- [x] **Persistence**: Persistence is achieved through sqlite.
|
||||||
- [x] **Dry-run**: Run the bot without playing money.
|
- [x] **Dry-run**: Run the bot without playing money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||||
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/).
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram.
|
||||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
||||||
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
||||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Quick start](#quick-start)
|
|
||||||
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
|
||||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
|
||||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
|
||||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
|
||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
|
||||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
|
||||||
- [Edge](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
|
||||||
- [Basic Usage](#basic-usage)
|
|
||||||
- [Bot commands](#bot-commands)
|
|
||||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
|
||||||
- [Support](#support)
|
|
||||||
- [Help](#help--slack)
|
|
||||||
- [Bugs](#bugs--issues)
|
|
||||||
- [Feature Requests](#feature-requests)
|
|
||||||
- [Pull Requests](#pull-requests)
|
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [Min hardware required](#min-hardware-required)
|
|
||||||
- [Software requirements](#software-requirements)
|
|
||||||
- [Wanna help?](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
|
||||||
- [Dev - getting started](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP)
|
|
||||||
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||||
|
@ -80,63 +60,52 @@ git checkout develop
|
||||||
./setup.sh --install
|
./setup.sh --install
|
||||||
```
|
```
|
||||||
|
|
||||||
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
|
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/).
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
|
||||||
|
|
||||||
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
|
||||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
|
||||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
|
||||||
- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
|
|
||||||
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
|
||||||
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
|
||||||
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
|
||||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
|
||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
### Bot commands
|
### Bot commands
|
||||||
|
|
||||||
```bash
|
```
|
||||||
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
[--strategy-path PATH] [--customhyperopt NAME]
|
||||||
[--dry-run-db]
|
[--dynamic-whitelist [INT]] [--db-url PATH]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,edge,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{backtesting,hyperopt}
|
{backtesting,edge,hyperopt}
|
||||||
backtesting backtesting module
|
backtesting backtesting module
|
||||||
|
edge edge module
|
||||||
hyperopt hyperopt module
|
hyperopt hyperopt module
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-v, --verbose be verbose
|
-v, --verbose verbose mode (-vv for more, -vvv to get all messages)
|
||||||
--version show program's version number and exit
|
--version show program\'s version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
-d PATH, --datadir PATH
|
-d PATH, --datadir PATH
|
||||||
path to backtest data (default:
|
path to backtest data
|
||||||
freqtrade/tests/testdata
|
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
specify strategy class name (default: DefaultStrategy)
|
specify strategy class name (default: DefaultStrategy)
|
||||||
--strategy-path PATH specify additional strategy lookup path
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
|
--customhyperopt NAME
|
||||||
|
specify hyperopt class name (default:
|
||||||
|
DefaultHyperOpts)
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (Default 20 currencies)
|
BaseVolume (default: 20) DEPRECATED.
|
||||||
--dry-run-db Force dry run to use a local DB
|
--db-url PATH Override trades database URL, this is useful if
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
dry_run is enabled or in custom deployments (default:
|
||||||
only if dry_run is enabled.
|
None)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
|
|
||||||
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/)
|
||||||
|
|
||||||
- `/start`: Starts the trader
|
- `/start`: Starts the trader
|
||||||
- `/stop`: Stops the trader
|
- `/stop`: Stops the trader
|
||||||
|
@ -227,4 +196,3 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
if [ ! -f "/usr/local/lib/libta_lib.a" ]; then
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib \
|
cd ta-lib \
|
||||||
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||||
|
@ -7,7 +7,5 @@ if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
&& which sudo && sudo make install || make install \
|
&& which sudo && sudo make install || make install \
|
||||||
&& cd ..
|
&& cd ..
|
||||||
else
|
else
|
||||||
echo "TA-lib already installed, skipping download and build."
|
echo "TA-lib already installed, skipping installation"
|
||||||
cd ta-lib && sudo make install && cd ..
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -13,7 +13,7 @@ if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then
|
||||||
else
|
else
|
||||||
echo "event ${TRAVIS_EVENT_TYPE}: building with cache"
|
echo "event ${TRAVIS_EVENT_TYPE}: building with cache"
|
||||||
# Pull last build to avoid rebuilding the whole image
|
# Pull last build to avoid rebuilding the whole image
|
||||||
docker pull ${REPO}:${TAG}
|
docker pull ${IMAGE_NAME}:${TAG}
|
||||||
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
|
"amount_reserve_percent" : 0.05,
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": "5m",
|
"ticker_interval": "5m",
|
||||||
"trailing_stop": false,
|
"trailing_stop": false,
|
||||||
|
@ -37,7 +38,8 @@
|
||||||
"buy": "limit",
|
"buy": "limit",
|
||||||
"sell": "limit",
|
"sell": "limit",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": "false"
|
"stoploss_on_exchange": "false",
|
||||||
|
"stoploss_on_exchange_interval": 60
|
||||||
},
|
},
|
||||||
"order_time_in_force": {
|
"order_time_in_force": {
|
||||||
"buy": "gtc",
|
"buy": "gtc",
|
||||||
|
|
|
@ -3,11 +3,6 @@
|
||||||
This page explains how to validate your strategy performance by using
|
This page explains how to validate your strategy performance by using
|
||||||
Backtesting.
|
Backtesting.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
|
||||||
- [Understand the backtesting result](#understand-the-backtesting-result)
|
|
||||||
|
|
||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies, you want to test it against
|
Now you have good Buy and Sell strategies, you want to test it against
|
||||||
|
@ -171,53 +166,65 @@ The most important in the backtesting is to understand the result.
|
||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
======================================== BACKTESTING REPORT =========================================
|
========================================================= BACKTESTING REPORT ========================================================
|
||||||
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|
||||||
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
||||||
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 21 |
|
||||||
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
|
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 8 |
|
||||||
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
|
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 14 |
|
||||||
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
|
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 7 |
|
||||||
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
|
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 10 |
|
||||||
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
|
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 20 |
|
||||||
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
|
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 15 |
|
||||||
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
|
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 17 |
|
||||||
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
|
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 18 |
|
||||||
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
|
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 9 |
|
||||||
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
|
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 21 |
|
||||||
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
|
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 7 |
|
||||||
====================================== LEFT OPEN TRADES REPORT ======================================
|
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 13 |
|
||||||
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 5 |
|
||||||
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 9 |
|
||||||
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
|
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 11 |
|
||||||
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
|
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 23 |
|
||||||
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
|
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 |
|
||||||
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||||
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
|
========================================================= SELL REASON STATS =========================================================
|
||||||
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
|
| Sell Reason | Count |
|
||||||
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
|:-------------------|--------:|
|
||||||
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
| trailing_stop_loss | 205 |
|
||||||
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
|
| stop_loss | 166 |
|
||||||
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
|
| sell_signal | 56 |
|
||||||
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
|
| force_sell | 2 |
|
||||||
|
====================================================== LEFT OPEN TRADES REPORT ======================================================
|
||||||
|
| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|
||||||
|
|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
||||||
|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 |
|
||||||
|
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 |
|
||||||
|
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 |
|
||||||
```
|
```
|
||||||
|
|
||||||
The 1st table will contain all trades the bot made.
|
The 1st table will contain all trades the bot made.
|
||||||
|
|
||||||
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
|
The 2nd table will contain a recap of sell reasons.
|
||||||
|
|
||||||
|
The 3rd table will contain all trades the bot had to `forcesell` at the end of the backtest period to present a full picture.
|
||||||
These trades are also included in the first table, but are extracted separately for clarity.
|
These trades are also included in the first table, but are extracted separately for clarity.
|
||||||
|
|
||||||
The last line will give you the overall performance of your strategy,
|
The last line will give you the overall performance of your strategy,
|
||||||
here:
|
here:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||||
```
|
```
|
||||||
|
|
||||||
We understand the bot has made `419` trades for an average duration of
|
We understand the bot has made `429` trades for an average duration of
|
||||||
`52.9` min, with a performance of `-0.41%` (loss), that means it has
|
`4:12:00`, with a performance of `76.20%` (profit), that means it has
|
||||||
lost a total of `-0.00348593 BTC`.
|
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
|
||||||
|
|
||||||
|
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums all the profits/losses.
|
||||||
|
The column `tot profit %` shows instead the total profit % in relation to allocated capital
|
||||||
|
(`max_open_trades * stake_amount`). In the above results we have `max_open_trades=2 stake_amount=0.005` in config
|
||||||
|
so `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
|
||||||
|
|
||||||
As you will see your strategy performance will be influenced by your buy
|
As you will see your strategy performance will be influenced by your buy
|
||||||
strategy, your sell strategy, and also by the `minimal_roi` and
|
strategy, your sell strategy, and also by the `minimal_roi` and
|
||||||
|
@ -256,15 +263,15 @@ There will be an additional table comparing win/losses of the different strategi
|
||||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
||||||
|
|
||||||
```
|
```
|
||||||
=================================================== Strategy Summary ====================================================
|
=========================================================== Strategy Summary ===========================================================
|
||||||
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss |
|
| Strategy | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|
||||||
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:|
|
|:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
||||||
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 |
|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||||
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 |
|
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 |
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
optimal parameters to use for your strategy?
|
optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](hyperopt.md)
|
||||||
|
|
|
@ -1,28 +1,8 @@
|
||||||
# Bot Optimization
|
# Optimization
|
||||||
|
|
||||||
This page explains where to customize your strategies, and add new
|
This page explains where to customize your strategies, and add new
|
||||||
indicators.
|
indicators.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
|
||||||
- [Customize your strategy](#change-your-strategy)
|
|
||||||
- [Anatomy of a strategy](#anatomy-of-a-strategy)
|
|
||||||
- [Customize indicators](#customize-indicators)
|
|
||||||
- [Buy signal rules](#buy-signal-rules)
|
|
||||||
- [Sell signal rules](#sell-signal-rules)
|
|
||||||
- [Minimal ROI](#minimal-roi)
|
|
||||||
- [Stoploss](#stoploss)
|
|
||||||
- [Ticker interval](#ticker-interval)
|
|
||||||
- [Metadata dict](#metadata-dict)
|
|
||||||
- [Where is the default strategy](#where-is-the-default-strategy)
|
|
||||||
- [Specify custom strategy location](#specify-custom-strategy-location)
|
|
||||||
- [Further strategy ideas](#further-strategy-ideas)
|
|
||||||
|
|
||||||
- [Where is the default strategy](#where-is-the-default-strategy)
|
|
||||||
|
|
||||||
Since the version `0.16.0` the bot allows using custom strategy file.
|
|
||||||
|
|
||||||
## Install a custom strategy file
|
## Install a custom strategy file
|
||||||
|
|
||||||
This is very simple. Copy paste your strategy file into the folder
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
|
@ -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`.
|
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`
|
You can test it with the parameter: `--strategy TestStrategy`
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
|
!!! Note: Strategies and Backtesting
|
||||||
|
To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
|
||||||
|
that during backtesting the full time-interval is passed to the `populate_*()` methods at once.
|
||||||
|
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
||||||
|
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
||||||
|
|
||||||
### Customize Indicators
|
### Customize Indicators
|
||||||
|
|
||||||
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
|
@ -118,10 +104,10 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Want more indicator examples
|
|
||||||
|
|
||||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
!!! Note "Want more indicator examples?"
|
||||||
Then uncomment indicators you need.
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).<br/>
|
||||||
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
### Buy signal rules
|
### Buy signal rules
|
||||||
|
|
||||||
|
@ -199,10 +185,9 @@ minimal_roi = {
|
||||||
The above configuration would therefore mean:
|
The above configuration would therefore mean:
|
||||||
|
|
||||||
- Sell whenever 4% profit was reached
|
- Sell whenever 4% profit was reached
|
||||||
- Sell after 20 minutes when 2% profit was reached
|
- Sell when 2% profit was reached (in effect after 20 minutes)
|
||||||
- Sell after 20 minutes when 2% profit was reached
|
- Sell when 1% profit was reached (in effect after 30 minutes)
|
||||||
- Sell after 30 minutes when 1% profit was reached
|
- Sell when trade is non-loosing (in effect after 40 minutes)
|
||||||
- Sell after 40 minutes when the trade is non-loosing (no profit)
|
|
||||||
|
|
||||||
The calculation does include fees.
|
The calculation does include fees.
|
||||||
|
|
||||||
|
@ -237,12 +222,129 @@ This is the set of candles the bot should download and use for the analysis.
|
||||||
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||||
|
|
||||||
Please note that the same buy/sell signals may work with one interval, but not the other.
|
Please note that the same buy/sell signals may work with one interval, but not the other.
|
||||||
|
This setting is accessible within the strategy by using `self.ticker_interval`.
|
||||||
|
|
||||||
### Metadata dict
|
### Metadata dict
|
||||||
|
|
||||||
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
||||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||||
|
|
||||||
|
The Metadata-dict should not be modified and does not persist information across multiple calls.
|
||||||
|
Instead, have a look at the section [Storing information](#Storing-information)
|
||||||
|
|
||||||
|
### Storing information
|
||||||
|
|
||||||
|
Storing information can be accomplished by crating a new dictionary within the strategy class.
|
||||||
|
|
||||||
|
The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Awesomestrategy(IStrategy):
|
||||||
|
# Create custom dictionary
|
||||||
|
cust_info = {}
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Check if the entry already exists
|
||||||
|
if "crosstime" in self.cust_info[metadata["pair"]:
|
||||||
|
self.cust_info[metadata["pair"]["crosstime"] += 1
|
||||||
|
else:
|
||||||
|
self.cust_info[metadata["pair"]["crosstime"] = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning:
|
||||||
|
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
|
||||||
|
|
||||||
|
!!! Note:
|
||||||
|
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
|
||||||
|
|
||||||
|
### Additional data (DataProvider)
|
||||||
|
|
||||||
|
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
|
||||||
|
|
||||||
|
!!!Note:
|
||||||
|
The DataProvier is currently not available during backtesting / hyperopt, but this is planned for the future.
|
||||||
|
|
||||||
|
All methods return `None` in case of failure (do not raise an exception).
|
||||||
|
|
||||||
|
Please always check if the `DataProvider` is available to avoid failures during backtesting.
|
||||||
|
|
||||||
|
#### Possible options for DataProvider
|
||||||
|
|
||||||
|
- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval)
|
||||||
|
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame
|
||||||
|
- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk
|
||||||
|
- `runmode` - Property containing the current runmode.
|
||||||
|
|
||||||
|
#### ohlcv / historic_ohlcv
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.dp:
|
||||||
|
if dp.runmode == 'live':
|
||||||
|
if ('ETH/BTC', ticker_interval) in self.dp.available_pairs:
|
||||||
|
data_eth = self.dp.ohlcv(pair='ETH/BTC',
|
||||||
|
ticker_interval=ticker_interval)
|
||||||
|
else:
|
||||||
|
# Get historic ohlcv data (cached on disk).
|
||||||
|
history_eth = self.dp.historic_ohlcv(pair='ETH/BTC',
|
||||||
|
ticker_interval='1h')
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning: Warning about backtesting
|
||||||
|
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go,
|
||||||
|
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
||||||
|
|
||||||
|
#### Available Pairs
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.dp:
|
||||||
|
for pair, ticker in self.dp.available_pairs:
|
||||||
|
print(f"available {pair}, {ticker}")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get data for non-tradeable pairs
|
||||||
|
|
||||||
|
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
|
||||||
|
Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see above).
|
||||||
|
These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting.
|
||||||
|
|
||||||
|
The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument.
|
||||||
|
|
||||||
|
Sample:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def informative_pairs(self):
|
||||||
|
return [("ETH/USDT", "5m"),
|
||||||
|
("BTC/TUSD", "15m"),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning:
|
||||||
|
As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
|
||||||
|
All intervals and all pairs can be specified as long as they are available (and active) on the used exchange.
|
||||||
|
It is however better to use resampling to longer time-intervals when possible
|
||||||
|
to avoid hammering the exchange with too many requests and risk beeing blocked.
|
||||||
|
|
||||||
|
### Additional data - Wallets
|
||||||
|
|
||||||
|
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||||
|
|
||||||
|
!!!NOTE:
|
||||||
|
Wallets is not available during backtesting / hyperopt.
|
||||||
|
|
||||||
|
Please always check if `Wallets` is available to avoid failures during backtesting.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.wallets:
|
||||||
|
free_eth = self.wallets.get_free('ETH')
|
||||||
|
used_eth = self.wallets.get_used('ETH')
|
||||||
|
total_eth = self.wallets.get_total('ETH')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Possible options for Wallets
|
||||||
|
|
||||||
|
- `get_free(asset)` - currently available balance to trade
|
||||||
|
- `get_used(asset)` - currently tied up balance (open orders)
|
||||||
|
- `get_total(asset)` - total available balance - sum of the 2 above
|
||||||
|
|
||||||
### Where is the default strategy?
|
### Where is the default strategy?
|
||||||
|
|
||||||
The default buy strategy is located in the file
|
The default buy strategy is located in the file
|
||||||
|
@ -267,4 +369,4 @@ We also got a *strategy-sharing* channel in our [Slack community](https://join.s
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Now you have a perfect strategy you probably want to backtest it.
|
Now you have a perfect strategy you probably want to backtest it.
|
||||||
Your next step is to learn [How to use the Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md).
|
Your next step is to learn [How to use the Backtesting](backtesting.md).
|
||||||
|
|
|
@ -1,32 +1,28 @@
|
||||||
# Bot usage
|
# Start the bot
|
||||||
|
|
||||||
This page explains the difference parameters of the bot and how to run it.
|
This page explains the different parameters of the bot and how to run it.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Bot commands](#bot-commands)
|
|
||||||
- [Backtesting commands](#backtesting-commands)
|
|
||||||
- [Hyperopt commands](#hyperopt-commands)
|
|
||||||
|
|
||||||
## Bot commands
|
## Bot commands
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
[--strategy-path PATH] [--customhyperopt NAME]
|
||||||
[--db-url PATH]
|
[--dynamic-whitelist [INT]] [--db-url PATH]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,edge,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Free, open source crypto trading bot
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{backtesting,hyperopt}
|
{backtesting,edge,hyperopt}
|
||||||
backtesting backtesting module
|
backtesting backtesting module
|
||||||
|
edge edge module
|
||||||
hyperopt hyperopt module
|
hyperopt hyperopt module
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-v, --verbose be verbose
|
-v, --verbose verbose mode (-vv for more, -vvv to get all messages)
|
||||||
--version show program's version number and exit
|
--version show program\'s version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
-d PATH, --datadir PATH
|
-d PATH, --datadir PATH
|
||||||
|
@ -34,12 +30,15 @@ optional arguments:
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
specify strategy class name (default: DefaultStrategy)
|
specify strategy class name (default: DefaultStrategy)
|
||||||
--strategy-path PATH specify additional strategy lookup path
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
|
--customhyperopt NAME
|
||||||
|
specify hyperopt class name (default:
|
||||||
|
DefaultHyperOpts)
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (default: 20) DEPRECATED
|
BaseVolume (default: 20) DEPRECATED.
|
||||||
--db-url PATH Override trades database URL, this is useful if
|
--db-url PATH Override trades database URL, this is useful if
|
||||||
dry_run is enabled or in custom deployments (default:
|
dry_run is enabled or in custom deployments (default:
|
||||||
sqlite:///tradesv3.sqlite)
|
None)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different config file?
|
### How to use a different config file?
|
||||||
|
@ -51,7 +50,7 @@ default, the bot will load the file `./config.json`
|
||||||
python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use --strategy?
|
### How to use **--strategy**?
|
||||||
|
|
||||||
This parameter will allow you to load your custom strategy class.
|
This parameter will allow you to load your custom strategy class.
|
||||||
Per default without `--strategy` or `-s` the bot will load the
|
Per default without `--strategy` or `-s` the bot will load the
|
||||||
|
@ -74,7 +73,7 @@ message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
### How to use --strategy-path?
|
### How to use **--strategy-path**?
|
||||||
|
|
||||||
This parameter allows you to add an additional strategy lookup path, which gets
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
checked before the default locations (The passed path must be a folder!):
|
checked before the default locations (The passed path must be a folder!):
|
||||||
|
@ -87,9 +86,10 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
|
||||||
This is very simple. Copy paste your strategy file into the folder
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
||||||
|
|
||||||
### How to use --dynamic-whitelist?
|
### How to use **--dynamic-whitelist**?
|
||||||
|
|
||||||
> Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](docs/configuration.md#Dynamic-Pairlists)
|
!!! danger "DEPRECATED"
|
||||||
|
Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](/configuration/#dynamic-pairlists)
|
||||||
|
|
||||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||||
on BaseVolume. This value can be changed when you run the script.
|
on BaseVolume. This value can be changed when you run the script.
|
||||||
|
@ -113,7 +113,7 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
|
||||||
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||||
value (20).
|
value (20).
|
||||||
|
|
||||||
### How to use --db-url?
|
### How to use **--db-url**?
|
||||||
|
|
||||||
When you run the bot in Dry-run mode, per default no transactions are
|
When you run the bot in Dry-run mode, per default no transactions are
|
||||||
stored in a database. If you want to store your bot actions in a DB
|
stored in a database. If you want to store your bot actions in a DB
|
||||||
|
@ -129,8 +129,8 @@ python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.s
|
||||||
Backtesting also uses the config specified via `-c/--config`.
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
|
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
|
||||||
[--timerange TIMERANGE] [-l] [-r]
|
[--eps] [--dmmp] [-l] [-r]
|
||||||
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
[--export EXPORT] [--export-filename PATH]
|
[--export EXPORT] [--export-filename PATH]
|
||||||
|
|
||||||
|
@ -138,6 +138,8 @@ optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
--eps, --enable-position-stacking
|
--eps, --enable-position-stacking
|
||||||
Allow buying the same pair multiple times (position
|
Allow buying the same pair multiple times (position
|
||||||
stacking)
|
stacking)
|
||||||
|
@ -145,8 +147,6 @@ optional arguments:
|
||||||
Disable applying `max_open_trades` during backtest
|
Disable applying `max_open_trades` during backtest
|
||||||
(same as setting `max_open_trades` to a very high
|
(same as setting `max_open_trades` to a very high
|
||||||
number)
|
number)
|
||||||
--timerange TIMERANGE
|
|
||||||
specify what timerange of data to use.
|
|
||||||
-l, --live using live data
|
-l, --live using live data
|
||||||
-r, --refresh-pairs-cached
|
-r, --refresh-pairs-cached
|
||||||
refresh the pairs files in tests/testdata with the
|
refresh the pairs files in tests/testdata with the
|
||||||
|
@ -167,18 +167,18 @@ optional arguments:
|
||||||
filename=user_data/backtest_data/backtest_today.json
|
filename=user_data/backtest_data/backtest_today.json
|
||||||
(default: user_data/backtest_data/backtest-
|
(default: user_data/backtest_data/backtest-
|
||||||
result.json)
|
result.json)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use --refresh-pairs-cached parameter?
|
### How to use **--refresh-pairs-cached** parameter?
|
||||||
|
|
||||||
The first time your run Backtesting, it will take the pairs you have
|
The first time your run Backtesting, it will take the pairs you have
|
||||||
set in your config file and download data from Bittrex.
|
set in your config file and download data from Bittrex.
|
||||||
|
|
||||||
If for any reason you want to update your data set, you use
|
If for any reason you want to update your data set, you use
|
||||||
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
||||||
**Use it only if you want to update your data set. You will not be able
|
|
||||||
to come back to the previous version.**
|
!!! Note
|
||||||
|
Use it only if you want to update your data set. You will not be able to come back to the previous version.
|
||||||
|
|
||||||
To test your strategy with latest data, we recommend continuing using
|
To test your strategy with latest data, we recommend continuing using
|
||||||
the parameter `-l` or `--live`.
|
the parameter `-l` or `--live`.
|
||||||
|
@ -250,4 +250,4 @@ in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
||||||
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
[optimize your bot](bot-optimization.md).
|
||||||
|
|
|
@ -2,12 +2,6 @@
|
||||||
|
|
||||||
This page explains how to configure your `config.json` file.
|
This page explains how to configure your `config.json` file.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Bot commands](#bot-commands)
|
|
||||||
- [Backtesting commands](#backtesting-commands)
|
|
||||||
- [Hyperopt commands](#hyperopt-commands)
|
|
||||||
|
|
||||||
## Setup config.json
|
## Setup config.json
|
||||||
|
|
||||||
We recommend to copy and use the `config.json.example` as a template
|
We recommend to copy and use the `config.json.example` as a template
|
||||||
|
@ -15,70 +9,98 @@ for your bot configuration.
|
||||||
|
|
||||||
The table below will list all configuration parameters.
|
The table below will list all configuration parameters.
|
||||||
|
|
||||||
| Command | Default | Mandatory | Description |
|
Mandatory Parameters are marked as **Required**.
|
||||||
|----------|---------|----------|-------------|
|
|
||||||
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
|
|
||||||
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
|
||||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
|
|
||||||
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
|
||||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
|
||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
|
||||||
| `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy.
|
|
||||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
|
||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
|
||||||
| `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
|
|
||||||
| `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached.
|
|
||||||
| `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
|
|
||||||
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
|
||||||
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
|
||||||
| `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids.
|
|
||||||
| `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
|
||||||
| `bid_strategy. check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
|
|
||||||
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
|
|
||||||
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
|
||||||
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
|
||||||
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
|
||||||
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types).
|
|
||||||
| `order_time_in_force` | None | No | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force).
|
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
|
||||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
|
||||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
|
||||||
| `exchange.ccxt_rate_limit` | True | No | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
|
||||||
| `exchange.ccxt_config` | None | No | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
|
||||||
| `exchange.ccxt_async_config` | None | No | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
|
||||||
| `edge` | false | No | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
|
||||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
|
||||||
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
|
||||||
| `pairlist.method` | StaticPairList | No | Use Static whitelist. [More information below](#dynamic-pairlists).
|
|
||||||
| `pairlist.config` | None | No | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
|
||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
|
||||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
|
||||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
|
||||||
| `webhook.enabled` | false | No | Enable usage of Webhook notifications
|
|
||||||
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
|
||||||
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
|
||||||
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
|
||||||
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
|
||||||
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
|
||||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
|
||||||
| `forcebuy_enable` | false | No | Enables the RPC Commands to force a buy. More information below.
|
|
||||||
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
|
||||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
|
||||||
|
|
||||||
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
| Command | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
|
||||||
|
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading.
|
||||||
|
| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance.
|
||||||
|
| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
|
||||||
|
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
|
||||||
|
| `dry_run` | true | **Required.** Define if the bot must be in Dry-run or production mode.
|
||||||
|
| `process_only_new_candles` | false | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `minimal_roi` | See below | Set the threshold in percent the bot will use to sell a trade. More information below. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `stoploss` | -0.10 | Value of the stoploss in percent used by the bot. More information below. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `trailing_stop` | false | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `trailing_stop_positive` | 0 | Changes stop-loss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `trailing_stop_positive_offset` | 0 | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `unfilledtimeout.buy` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
||||||
|
| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
||||||
|
| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance).
|
||||||
|
| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids.
|
||||||
|
| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
||||||
|
| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
|
||||||
|
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
|
||||||
|
| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||||
|
| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
|
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
|
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `exchange.name` | bittrex | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
|
| `exchange.key` | key | API key to use for the exchange. Only required when you are in production mode.
|
||||||
|
| `exchange.secret` | secret | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
|
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
|
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
|
| `exchange.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
||||||
|
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||||
|
| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||||
|
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||||
|
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-strategy).
|
||||||
|
| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists).
|
||||||
|
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
||||||
|
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
|
||||||
|
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||||
|
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
|
| `webhook.enabled` | false | Enable usage of Webhook notifications
|
||||||
|
| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhooksell` | false | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhookstatus` | false | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
|
| `db_url` | `sqlite:///tradesv3.sqlite`| Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
||||||
|
| `initial_state` | running | Defines the initial application state. More information below.
|
||||||
|
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
|
||||||
|
| `strategy` | DefaultStrategy | Defines Strategy class to use.
|
||||||
|
| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder).
|
||||||
|
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
|
||||||
|
|
||||||
|
### Parameters in strategy
|
||||||
|
|
||||||
|
The following parameters can be set in either configuration or strategy.
|
||||||
|
Values in the configuration are always overwriting values set in the strategy.
|
||||||
|
|
||||||
|
* `minimal_roi`
|
||||||
|
* `ticker_interval`
|
||||||
|
* `stoploss`
|
||||||
|
* `trailing_stop`
|
||||||
|
* `trailing_stop_positive`
|
||||||
|
* `trailing_stop_positive_offset`
|
||||||
|
* `process_only_new_candles`
|
||||||
|
* `order_types`
|
||||||
|
* `order_time_in_force`
|
||||||
|
* `use_sell_signal` (experimental)
|
||||||
|
* `sell_profit_only` (experimental)
|
||||||
|
* `ignore_roi_if_buy_signal` (experimental)
|
||||||
|
|
||||||
### Understand stake_amount
|
### Understand stake_amount
|
||||||
|
|
||||||
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
||||||
The minimal value is 0.0005. If there is not enough crypto-currency in
|
The minimal value is 0.0005. If there is not enough crypto-currency in
|
||||||
the account an exception is generated.
|
the account an exception is generated.
|
||||||
To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`.
|
To allow the bot to trade all the available `stake_currency` in your account set
|
||||||
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
|
||||||
|
```json
|
||||||
|
"stake_amount" : "unlimited",
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case a trade amount is calclulated as:
|
||||||
|
|
||||||
|
```python
|
||||||
|
currency_balanse / (max_open_trades - current_open_trades)
|
||||||
|
```
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
|
|
||||||
|
@ -86,7 +108,7 @@ In this case a trade amount is calclulated as `currency_balanse / (max_open_trad
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
|
|
||||||
```
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
||||||
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit
|
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit
|
||||||
|
@ -144,26 +166,31 @@ end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
### Understand order_types
|
### Understand order_types
|
||||||
|
|
||||||
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
|
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type and stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically and update it if necessary (e.x. in case of trailing stoploss).
|
||||||
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
|
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
|
||||||
|
|
||||||
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
|
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"` and `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
|
||||||
The below is the default which is used if this is not configured in either Strategy or configuration.
|
The below is the default which is used if this is not configured in either Strategy or configuration.
|
||||||
|
|
||||||
``` python
|
```python
|
||||||
"order_types": {
|
"order_types": {
|
||||||
"buy": "limit",
|
"buy": "limit",
|
||||||
"sell": "limit",
|
"sell": "limit",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": False
|
"stoploss_on_exchange": False,
|
||||||
},
|
"stoploss_on_exchange_interval": 60
|
||||||
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
**NOTE**: Not all exchanges support "market" orders.
|
!!! Note
|
||||||
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
|
Not all exchanges support "market" orders.
|
||||||
|
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
stoploss on exchange interval is not mandatory. Do not change it's value if you are unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md).
|
||||||
|
|
||||||
### Understand order_time_in_force
|
### Understand order_time_in_force
|
||||||
Order time in force defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
|
`order_time_in_force` defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
|
||||||
**GTC (Goog Till Canceled):**
|
**GTC (Goog Till Canceled):**
|
||||||
This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/>
|
This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/>
|
||||||
**FOK (Full Or Kill):**
|
**FOK (Full Or Kill):**
|
||||||
|
@ -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.<br/>
|
`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.<br/>
|
||||||
possible values are: `gtc` (default), `fok` or `ioc`.<br/>
|
possible values are: `gtc` (default), `fok` or `ioc`.<br/>
|
||||||
``` python
|
``` python
|
||||||
"order_time_in_force": {
|
"order_time_in_force": {
|
||||||
"buy": "gtc",
|
"buy": "gtc",
|
||||||
"sell": "gtc"
|
"sell": "gtc"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
**NOTE**: This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.<br/>
|
|
||||||
|
!!! Warning
|
||||||
|
This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.
|
||||||
|
|
||||||
### What values for exchange.name?
|
### What values for exchange.name?
|
||||||
|
|
||||||
|
@ -198,9 +227,15 @@ Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
### What values for fiat_display_currency?
|
### What values for fiat_display_currency?
|
||||||
|
|
||||||
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are:<br/>
|
||||||
In addition to central bank currencies, a range of cryto currencies are supported.
|
```json
|
||||||
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
||||||
|
```
|
||||||
|
In addition to FIAT currencies, a range of cryto currencies are supported.
|
||||||
|
The valid values are:
|
||||||
|
```json
|
||||||
|
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
|
```
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run mode
|
||||||
|
|
||||||
|
@ -209,14 +244,12 @@ behave and how is the performance of your strategy. In Dry-run mode the
|
||||||
bot does not engage your money. It only runs a live simulation without
|
bot does not engage your money. It only runs a live simulation without
|
||||||
creating trades.
|
creating trades.
|
||||||
|
|
||||||
### To switch your bot in Dry-run mode:
|
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
2. Switch dry-run to true and specify db_url for a persistent db
|
2. Switch dry-run to true and specify db_url for a persistent db
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
"db_url": "sqlite:///tradesv3.dryrun.sqlite",
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove your Exchange API key (change them by fake api credentials)
|
3. Remove your Exchange API key (change them by fake api credentials)
|
||||||
|
@ -238,9 +271,9 @@ production mode.
|
||||||
Dynamic pairlists select pairs for you based on the logic configured.
|
Dynamic pairlists select pairs for you based on the logic configured.
|
||||||
The bot runs against all pairs (with that stake) on the exchange, and a number of assets (`number_assets`) is selected based on the selected criteria.
|
The bot runs against all pairs (with that stake) on the exchange, and a number of assets (`number_assets`) is selected based on the selected criteria.
|
||||||
|
|
||||||
By *default*, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration).
|
By default, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration).
|
||||||
|
|
||||||
#### Available Pairlist methods
|
**Available Pairlist methods:**
|
||||||
|
|
||||||
* `"StaticPairList"`
|
* `"StaticPairList"`
|
||||||
* uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`
|
* uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`
|
||||||
|
@ -266,15 +299,15 @@ you run it in production mode.
|
||||||
|
|
||||||
### To switch your bot in production mode
|
### To switch your bot in production mode
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
**Edit your `config.json` file.**
|
||||||
|
|
||||||
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
**Switch dry-run to false and don't forget to adapt your database URL if set:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert your Exchange API key (change them by fake api keys)
|
**Insert your Exchange API key (change them by fake api keys):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
|
@ -285,8 +318,8 @@ you run it in production mode.
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
!!! Note
|
||||||
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
If you have an exchange API key yet, [see our tutorial](/pre-requisite).
|
||||||
|
|
||||||
### Using proxy with FreqTrade
|
### Using proxy with FreqTrade
|
||||||
|
|
||||||
|
@ -337,4 +370,4 @@ Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).
|
||||||
|
|
|
@ -4,8 +4,20 @@ This page is intended for developers of FreqTrade, people who want to contribute
|
||||||
|
|
||||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
|
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
## Module
|
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
||||||
|
|
||||||
|
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
|
||||||
|
|
||||||
|
## Developer setup
|
||||||
|
|
||||||
|
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
|
||||||
|
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`.
|
||||||
|
|
||||||
|
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
### Dynamic Pairlist
|
### Dynamic Pairlist
|
||||||
|
|
||||||
|
@ -68,3 +80,38 @@ Please also run `self._validate_whitelist(pairs)` and to check and remove pairs
|
||||||
|
|
||||||
This is a simple method used by `VolumePairList` - however serves as a good example.
|
This is a simple method used by `VolumePairList` - however serves as a good example.
|
||||||
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider.
|
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider.
|
||||||
|
|
||||||
|
## Creating a release
|
||||||
|
|
||||||
|
This part of the documentation is aimed at maintainers, and shows how to create a release.
|
||||||
|
|
||||||
|
### create release branch
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# make sure you're in develop branch
|
||||||
|
git checkout develop
|
||||||
|
|
||||||
|
# create new branch
|
||||||
|
git checkout -b new_release
|
||||||
|
```
|
||||||
|
|
||||||
|
* edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`)
|
||||||
|
* Commit this part
|
||||||
|
* push that branch to the remote and create a PR
|
||||||
|
|
||||||
|
### create changelog from git commits
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# Needs to be done before merging / pulling that branch.
|
||||||
|
git log --oneline --no-decorate --no-merges master..develop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create github release / tag
|
||||||
|
|
||||||
|
* Use the version-number specified as tag.
|
||||||
|
* Use "master" as reference (this step comes after the above PR is merged).
|
||||||
|
* use the above changelog as release comment (as codeblock)
|
||||||
|
|
||||||
|
### After-release
|
||||||
|
|
||||||
|
* update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`)
|
||||||
|
|
14
docs/edge.md
14
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.
|
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
|
||||||
|
|
||||||
**NOTICE:** Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist.
|
!!! Warning
|
||||||
**NOTICE2:** Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation.
|
Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist.
|
||||||
|
|
||||||
## Table of Contents
|
!!! Note
|
||||||
|
Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation.
|
||||||
- [Introduction](#introduction)
|
|
||||||
- [How does it work?](#how-does-it-work?)
|
|
||||||
- [Configurations](#configurations)
|
|
||||||
- [Running Edge independently](#running-edge-independently)
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/>
|
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/>
|
||||||
|
@ -28,7 +24,7 @@ The answer comes to two factors:
|
||||||
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
|
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
|
||||||
|
|
||||||
|
|
||||||
`W = (Number of winning trades) / (Number of losing trades)`
|
`W = (Number of winning trades) / (Total number of trades)`
|
||||||
|
|
||||||
### Risk Reward Ratio
|
### Risk Reward Ratio
|
||||||
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
|
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
|
||||||
|
|
|
@ -27,7 +27,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
|
||||||
#### I want to improve the bot with a new strategy
|
#### I want to improve the bot with a new strategy
|
||||||
|
|
||||||
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||||
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
|
||||||
|
|
||||||
#### Is there a setting to only SELL the coins being held and not
|
#### Is there a setting to only SELL the coins being held and not
|
||||||
perform anymore BUYS?
|
perform anymore BUYS?
|
||||||
|
@ -68,4 +68,3 @@ but it will give the idea. With only these triggers and guards there is
|
||||||
already 8*10^9*10 evaluations. A roughly total of 80 billion evals.
|
already 8*10^9*10 evaluations. A roughly total of 80 billion evals.
|
||||||
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
|
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
|
||||||
of the search space.
|
of the search space.
|
||||||
|
|
||||||
|
|
112
docs/hyperopt.md
112
docs/hyperopt.md
|
@ -6,43 +6,50 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||||
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||||
and still take a long time.
|
and still take a long time.
|
||||||
|
|
||||||
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
!!! Bug
|
||||||
|
Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
|
||||||
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
|
||||||
- [Solving a Mystery](#solving-a-mystery)
|
|
||||||
- [Adding New Indicators](#adding-new-indicators)
|
|
||||||
- [Execute Hyperopt](#execute-hyperopt)
|
|
||||||
- [Understand the hyperopt result](#understand-the-hyperopt-result)
|
|
||||||
|
|
||||||
## Prepare Hyperopting
|
## Prepare Hyperopting
|
||||||
|
|
||||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
Before we start digging into Hyperopt, we recommend you to take a look at
|
||||||
an example hyperopt file located into [user_data/hyperopts/](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py)
|
an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py)
|
||||||
|
|
||||||
|
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
|
||||||
|
|
||||||
|
### Checklist on all tasks / possibilities in hyperopt
|
||||||
|
|
||||||
|
Depending on the space you want to optimize, only some of the below are required.
|
||||||
|
|
||||||
|
* fill `populate_indicators` - probably a copy from your strategy
|
||||||
|
* fill `buy_strategy_generator` - for buy signal optimization
|
||||||
|
* fill `indicator_space` - for buy signal optimzation
|
||||||
|
* fill `sell_strategy_generator` - for sell signal optimization
|
||||||
|
* fill `sell_indicator_space` - for sell signal optimzation
|
||||||
|
* fill `roi_space` - for ROI optimization
|
||||||
|
* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries)
|
||||||
|
* fill `stoploss_space` - stoploss optimization
|
||||||
|
* Optional but recommended
|
||||||
|
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
||||||
|
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
||||||
|
|
||||||
### 1. Install a Custom Hyperopt File
|
### 1. Install a Custom Hyperopt File
|
||||||
This is very simple. Put your hyperopt file into the folder
|
|
||||||
`user_data/hyperopts`.
|
Put your hyperopt file into the folder`user_data/hyperopts`.
|
||||||
|
|
||||||
Let assume you want a hyperopt file `awesome_hyperopt.py`:
|
Let assume you want a hyperopt file `awesome_hyperopt.py`:
|
||||||
1. Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
|
Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
|
||||||
|
|
||||||
|
|
||||||
### 2. Configure your Guards and Triggers
|
### 2. Configure your Guards and Triggers
|
||||||
There are two places you need to change in your hyperopt file to add a
|
|
||||||
new buy hyperopt for testing:
|
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
|
||||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L230-L251).
|
|
||||||
- Inside [indicator_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L207-L223).
|
- Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
|
||||||
|
- Inside `populate_buy_trend()` - applying the parameters.
|
||||||
|
|
||||||
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
|
|
||||||
1. Guards are conditions like "never buy if ADX < 10", or never buy if
|
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
|
||||||
current price is over EMA10.
|
2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower bollinger band".
|
||||||
2. Triggers are ones that actually trigger buy in specific moment, like
|
|
||||||
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
|
||||||
bollinger band".
|
|
||||||
|
|
||||||
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
||||||
multiple guards. The constructed strategy will be something like
|
multiple guards. The constructed strategy will be something like
|
||||||
|
@ -53,6 +60,17 @@ If you have updated the buy strategy, ie. changed the contents of
|
||||||
`populate_buy_trend()` method you have to update the `guards` and
|
`populate_buy_trend()` method you have to update the `guards` and
|
||||||
`triggers` hyperopts must use.
|
`triggers` hyperopts must use.
|
||||||
|
|
||||||
|
#### Sell optimization
|
||||||
|
|
||||||
|
Similar to the buy-signal above, sell-signals can also be optimized.
|
||||||
|
Place the corresponding settings into the following methods
|
||||||
|
|
||||||
|
* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing.
|
||||||
|
* Inside `populate_sell_trend()` - applying the parameters.
|
||||||
|
|
||||||
|
The configuration and rules are the same than for buy signals.
|
||||||
|
To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`.
|
||||||
|
|
||||||
## Solving a Mystery
|
## Solving a Mystery
|
||||||
|
|
||||||
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
||||||
|
@ -63,7 +81,7 @@ mystery.
|
||||||
|
|
||||||
We will start by defining a search space:
|
We will start by defining a search space:
|
||||||
|
|
||||||
```
|
```python
|
||||||
def indicator_space() -> List[Dimension]:
|
def indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching strategy parameters
|
Define your Hyperopt space for searching strategy parameters
|
||||||
|
@ -86,7 +104,7 @@ one we call `trigger` and use it to decide which buy trigger we want to use.
|
||||||
|
|
||||||
So let's write the buy strategy using these values:
|
So let's write the buy strategy using these values:
|
||||||
|
|
||||||
```
|
``` python
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
|
@ -96,6 +114,7 @@ So let's write the buy strategy using these values:
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||||
|
|
||||||
# TRIGGERS
|
# TRIGGERS
|
||||||
|
if 'trigger' in params:
|
||||||
if params['trigger'] == 'bb_lower':
|
if params['trigger'] == 'bb_lower':
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
|
@ -133,15 +152,19 @@ Because hyperopt tries a lot of combinations to find the best parameters it will
|
||||||
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -s <strategyname> --hyperopt <hyperoptname> -c config.json hyperopt -e 5000
|
python3 ./freqtrade/main.py --hyperopt <hyperoptname> -c config.json hyperopt -e 5000 --spaces all
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `<strategyname>` and `<hyperoptname>` as the names of the custom strategy
|
Use `<hyperoptname>` as the name of the custom hyperopt used.
|
||||||
(only required for generating sells) and the custom hyperopt used.
|
|
||||||
|
|
||||||
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||||
running at least several thousand evaluations.
|
running at least several thousand evaluations.
|
||||||
|
|
||||||
|
The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file.
|
||||||
|
|
||||||
### Execute Hyperopt with Different Ticker-Data Source
|
### Execute Hyperopt with Different Ticker-Data Source
|
||||||
|
|
||||||
If you would like to hyperopt parameters using an alternate ticker data that
|
If you would like to hyperopt parameters using an alternate ticker data that
|
||||||
|
@ -170,6 +193,7 @@ Legal values are:
|
||||||
|
|
||||||
- `all`: optimize everything
|
- `all`: optimize everything
|
||||||
- `buy`: just search for a new buy strategy
|
- `buy`: just search for a new buy strategy
|
||||||
|
- `sell`: just search for a new sell strategy
|
||||||
- `roi`: just optimize the minimal profit table for your strategy
|
- `roi`: just optimize the minimal profit table for your strategy
|
||||||
- `stoploss`: search for the best stoploss value
|
- `stoploss`: search for the best stoploss value
|
||||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
@ -183,7 +207,11 @@ Given the following result from hyperopt:
|
||||||
Best result:
|
Best result:
|
||||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||||
with values:
|
with values:
|
||||||
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
|
{ 'adx-value': 44,
|
||||||
|
'rsi-value': 29,
|
||||||
|
'adx-enabled': False,
|
||||||
|
'rsi-enabled': True,
|
||||||
|
'trigger': 'bb_lower'}
|
||||||
```
|
```
|
||||||
|
|
||||||
You should understand this result like:
|
You should understand this result like:
|
||||||
|
@ -223,9 +251,24 @@ If you are optimizing ROI, you're result will look as follows and include a ROI
|
||||||
Best result:
|
Best result:
|
||||||
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||||
with values:
|
with values:
|
||||||
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower', 'roi_t1': 40, 'roi_t2': 57, 'roi_t3': 21, 'roi_p1': 0.03634636907306948, 'roi_p2': 0.055237357937802885, 'roi_p3': 0.015163796015548354, 'stoploss': -0.37996664668703606}
|
{ 'adx-value': 44,
|
||||||
|
'rsi-value': 29,
|
||||||
|
'adx-enabled': false,
|
||||||
|
'rsi-enabled': True,
|
||||||
|
'trigger': 'bb_lower',
|
||||||
|
'roi_t1': 40,
|
||||||
|
'roi_t2': 57,
|
||||||
|
'roi_t3': 21,
|
||||||
|
'roi_p1': 0.03634636907306948,
|
||||||
|
'roi_p2': 0.055237357937802885,
|
||||||
|
'roi_p3': 0.015163796015548354,
|
||||||
|
'stoploss': -0.37996664668703606
|
||||||
|
}
|
||||||
ROI table:
|
ROI table:
|
||||||
{0: 0.10674752302642071, 21: 0.09158372701087236, 78: 0.03634636907306948, 118: 0}
|
{ 0: 0.10674752302642071,
|
||||||
|
21: 0.09158372701087236,
|
||||||
|
78: 0.03634636907306948,
|
||||||
|
118: 0}
|
||||||
```
|
```
|
||||||
|
|
||||||
This would translate to the following ROI table:
|
This would translate to the following ROI table:
|
||||||
|
@ -245,9 +288,10 @@ Once the optimized strategy has been implemented into your strategy, you should
|
||||||
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`.
|
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`.
|
||||||
This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283).
|
This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283).
|
||||||
|
|
||||||
|
!!! Note:
|
||||||
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
||||||
|
|
||||||
## Next Step
|
## Next Step
|
||||||
|
|
||||||
Now you have a perfect bot and want to control it from Telegram. Your
|
Now you have a perfect bot and want to control it from Telegram. Your
|
||||||
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
next step is to learn the [Telegram usage](telegram-usage.md).
|
||||||
|
|
BIN
docs/images/logo.png
Normal file
BIN
docs/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
100
docs/index.md
100
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
|
<!-- Place this tag where you want the button to render. -->
|
||||||
this documentation if you see it became outdated by sending us a
|
<a class="github-button" href="https://github.com/freqtrade/freqtrade" data-icon="octicon-star" data-size="large" aria-label="Star freqtrade/freqtrade on GitHub">Star</a>
|
||||||
Pull-request. Do not hesitate to reach us on
|
<!-- Place this tag where you want the button to render. -->
|
||||||
[Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
<a class="github-button" href="https://github.com/freqtrade/freqtrade/fork" data-icon="octicon-repo-forked" data-size="large" aria-label="Fork freqtrade/freqtrade on GitHub">Fork</a>
|
||||||
if you do not find the answer to your questions.
|
<!-- Place this tag where you want the button to render. -->
|
||||||
|
<a class="github-button" href="https://github.com/freqtrade/freqtrade/archive/master.zip" data-icon="octicon-cloud-download" data-size="large" aria-label="Download freqtrade/freqtrade on GitHub">Download</a>
|
||||||
|
<!-- Place this tag where you want the button to render. -->
|
||||||
|
<a class="github-button" href="https://github.com/freqtrade" data-size="large" aria-label="Follow @freqtrade on GitHub">Follow @freqtrade</a>
|
||||||
|
## Introduction
|
||||||
|
Freqtrade is a cryptocurrency trading bot written in Python.
|
||||||
|
|
||||||
## Table of Contents
|
!!! Danger "DISCLAIMER"
|
||||||
|
This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||||
|
|
||||||
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
Always start by running a trading bot in Dry-run and do not engage money before you understand how it works and what profit/loss you should expect.
|
||||||
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
|
||||||
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot.
|
||||||
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
|
||||||
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
|
|
||||||
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
|
## Features
|
||||||
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
|
- Based on Python 3.6+: For botting on any operating system - Windows, macOS and Linux
|
||||||
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows)
|
- Persistence: Persistence is achieved through sqlite
|
||||||
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
- Dry-run: Run the bot without playing money.
|
||||||
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
|
- Backtesting: Run a simulation of your buy/sell strategy.
|
||||||
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- Strategy Optimization by machine learning: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||||
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
- Edge position sizing Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. Learn more
|
||||||
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||||
- [Edge commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#edge-commands)
|
- Blacklist crypto-currencies: Select which crypto-currency you want to avoid.
|
||||||
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
- Manageable via Telegram: Manage the bot with Telegram
|
||||||
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
- Display profit/loss in fiat: Display your profit/loss in 33 fiat.
|
||||||
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
- Daily summary of profit/loss: Provide a daily summary of your profit/loss.
|
||||||
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- Performance status report: Provide a performance status of your current trades.
|
||||||
- [Edge positioning](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
|
||||||
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
|
||||||
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
## Requirements
|
||||||
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
|
### Uptodate clock
|
||||||
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||||
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
|
||||||
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
### Hardware requirements
|
||||||
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
|
||||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
- 2GB RAM
|
||||||
- [Developer Docs](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md)
|
- 1GB disk space
|
||||||
|
- 2vCPU
|
||||||
|
|
||||||
|
### Software requirements
|
||||||
|
- Python 3.6.x
|
||||||
|
- pip
|
||||||
|
- git
|
||||||
|
- TA-Lib
|
||||||
|
- virtualenv (Recommended)
|
||||||
|
- Docker (Recommended)
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
Help / Slack
|
||||||
|
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel.
|
||||||
|
|
||||||
|
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) to join Slack channel.
|
||||||
|
|
||||||
|
## Ready to try?
|
||||||
|
Begin by reading our installation guide [here](installation).
|
|
@ -1,25 +1,69 @@
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
This page explains how to prepare your environment for running the bot.
|
This page explains how to prepare your environment for running the bot.
|
||||||
|
|
||||||
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
## Prerequisite
|
||||||
|
Before running your bot in production you will need to setup few
|
||||||
|
external API. In production mode, the bot required valid Bittrex API
|
||||||
|
credentials and a Telegram bot (optional but recommended).
|
||||||
|
|
||||||
## Table of Contents
|
- [Setup your exchange account](#setup-your-exchange-account)
|
||||||
|
- [Backtesting commands](#setup-your-telegram-bot)
|
||||||
|
|
||||||
* [Table of Contents](#table-of-contents)
|
### Setup your exchange account
|
||||||
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
*To be completed, please feel free to complete this section.*
|
||||||
* [Automatic Installation - Docker](#automatic-installation---docker)
|
|
||||||
* [Custom Linux MacOS Installation](#custom-installation)
|
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [Linux - Ubuntu 16.04](#linux---ubuntu-1604)
|
|
||||||
- [MacOS](#macos)
|
|
||||||
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
|
||||||
* [Windows](#windows)
|
|
||||||
|
|
||||||
<!-- /TOC -->
|
### Setup your Telegram bot
|
||||||
|
The only things you need is a working Telegram bot and its API token.
|
||||||
|
Below we explain how to create your Telegram Bot, and how to get your
|
||||||
|
Telegram user id.
|
||||||
|
|
||||||
------
|
### 1. Create your Telegram bot
|
||||||
|
|
||||||
|
**1.1. Start a chat with https://telegram.me/BotFather**
|
||||||
|
|
||||||
|
**1.2. Send the message `/newbot`. ** *BotFather response:*
|
||||||
|
```
|
||||||
|
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.3. Choose the public name of your bot (e.x. `Freqtrade bot`)**
|
||||||
|
*BotFather response:*
|
||||||
|
```
|
||||||
|
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
|
||||||
|
```
|
||||||
|
**1.4. Choose the name id of your bot (e.x "`My_own_freqtrade_bot`")**
|
||||||
|
|
||||||
|
**1.5. Father bot will return you the token (API key)**<br/>
|
||||||
|
Copy it and keep it you will use it for the config parameter `token`.
|
||||||
|
*BotFather response:*
|
||||||
|
```hl_lines="4"
|
||||||
|
Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
|
||||||
|
|
||||||
|
Use this token to access the HTTP API:
|
||||||
|
521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0
|
||||||
|
|
||||||
|
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
|
||||||
|
```
|
||||||
|
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
|
||||||
|
|
||||||
|
### 2. Get your user id
|
||||||
|
**2.1. Talk to https://telegram.me/userinfobot**
|
||||||
|
|
||||||
|
**2.2. Get your "Id", you will use it for the config parameter
|
||||||
|
`chat_id`.**
|
||||||
|
<hr/>
|
||||||
|
## Quick start
|
||||||
|
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
|
cd freqtrade
|
||||||
|
git checkout develop
|
||||||
|
./setup.sh --install
|
||||||
|
```
|
||||||
|
!!! Note
|
||||||
|
Windows installation is explained [here](#windows).
|
||||||
|
<hr/>
|
||||||
## Easy Installation - Linux Script
|
## Easy Installation - Linux Script
|
||||||
|
|
||||||
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
||||||
|
@ -33,7 +77,7 @@ usage:
|
||||||
-c,--config Easy config generator (Will override your existing file).
|
-c,--config Easy config generator (Will override your existing file).
|
||||||
```
|
```
|
||||||
|
|
||||||
### --install
|
** --install **
|
||||||
|
|
||||||
This script will install everything you need to run the bot:
|
This script will install everything you need to run the bot:
|
||||||
|
|
||||||
|
@ -43,15 +87,15 @@ This script will install everything you need to run the bot:
|
||||||
|
|
||||||
This script is a combination of `install script` `--reset`, `--config`
|
This script is a combination of `install script` `--reset`, `--config`
|
||||||
|
|
||||||
### --update
|
** --update **
|
||||||
|
|
||||||
Update parameter will pull the last version of your current branch and update your virtualenv.
|
Update parameter will pull the last version of your current branch and update your virtualenv.
|
||||||
|
|
||||||
### --reset
|
** --reset **
|
||||||
|
|
||||||
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
|
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
|
||||||
|
|
||||||
### --config
|
** --config **
|
||||||
|
|
||||||
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
||||||
|
|
||||||
|
@ -69,33 +113,39 @@ Once you have Docker installed, simply create the config file (e.g. `config.json
|
||||||
|
|
||||||
### 1. Prepare the Bot
|
### 1. Prepare the Bot
|
||||||
|
|
||||||
#### 1.1. Clone the git repository
|
**1.1. Clone the git repository**
|
||||||
|
|
||||||
|
Linux/Mac/Windows with WSL
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.2. (Optional) Checkout the develop branch
|
Windows with docker
|
||||||
|
```bash
|
||||||
|
git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.2. (Optional) Checkout the develop branch**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout develop
|
git checkout develop
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.3. Go into the new directory
|
**1.3. Go into the new directory**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.4. Copy `config.json.example` to `config.json`
|
**1.4. Copy `config.json.example` to `config.json`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp -n config.json.example config.json
|
cp -n config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
> To edit the config please refer to the [Bot Configuration](configuration.md) page.
|
||||||
|
|
||||||
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
**1.5. Create your database file *(optional - the bot will create it if it is missing)**
|
||||||
|
|
||||||
Production
|
Production
|
||||||
|
|
||||||
|
@ -115,7 +165,7 @@ Either use the prebuilt image from docker hub - or build the image yourself if y
|
||||||
|
|
||||||
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
|
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
|
||||||
|
|
||||||
#### 2.1. Download the docker image
|
**2.1. Download the docker image**
|
||||||
|
|
||||||
Pull the image from docker hub and (optionally) change the name of the image
|
Pull the image from docker hub and (optionally) change the name of the image
|
||||||
|
|
||||||
|
@ -127,7 +177,7 @@ docker tag freqtradeorg/freqtrade:develop freqtrade
|
||||||
|
|
||||||
To update the image, simply run the above commands again and restart your running container.
|
To update the image, simply run the above commands again and restart your running container.
|
||||||
|
|
||||||
#### 2.2. Build the Docker image
|
**2.2. Build the Docker image**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
|
@ -164,7 +214,7 @@ There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtim
|
||||||
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396)
|
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396).
|
||||||
|
|
||||||
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
||||||
|
|
||||||
|
@ -172,7 +222,7 @@ In this example, the database will be created inside the docker instance and wil
|
||||||
|
|
||||||
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
|
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
|
||||||
|
|
||||||
#### 5.1. Move your config file and database
|
**5.1. Move your config file and database**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir ~/.freqtrade
|
mkdir ~/.freqtrade
|
||||||
|
@ -180,7 +230,7 @@ mv config.json ~/.freqtrade
|
||||||
mv tradesv3.sqlite ~/.freqtrade
|
mv tradesv3.sqlite ~/.freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5.2. Run the docker image
|
**5.2. Run the docker image**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
|
@ -191,8 +241,9 @@ docker run -d \
|
||||||
freqtrade --db-url sqlite:///tradesv3.sqlite
|
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note*: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
!!! Note
|
||||||
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
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
|
### 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/).
|
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
|
||||||
|
|
||||||
*Note*: You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
!!! Note
|
||||||
|
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
### 7. Backtest with docker
|
### 7. Backtest with docker
|
||||||
|
|
||||||
The following assumes that the above steps (1-4) have been completed successfully.
|
The following assumes that the above steps (1-4) have been completed successfully.
|
||||||
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name freqtrade \
|
--name freqtrade \
|
||||||
-v /etc/localtime:/etc/localtime:ro \
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
|
@ -225,16 +277,17 @@ docker run -d \
|
||||||
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
||||||
```
|
```
|
||||||
|
|
||||||
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details.
|
Head over to the [Backtesting Documentation](backtesting.md) for more details.
|
||||||
|
|
||||||
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example).
|
!!! Note
|
||||||
|
Additional parameters can be appended after the image name (`freqtrade` in the above example).
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Custom Installation
|
## Custom Installation
|
||||||
|
|
||||||
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||||
OS Specific steps are listed first, the [common](#common) section below is necessary for all systems.
|
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
|
@ -286,7 +339,7 @@ python3 -m pip install -e .
|
||||||
brew install python3 git wget
|
brew install python3 git wget
|
||||||
```
|
```
|
||||||
|
|
||||||
### common
|
### Common
|
||||||
|
|
||||||
#### 1. Install TA-Lib
|
#### 1. Install TA-Lib
|
||||||
|
|
||||||
|
@ -304,11 +357,13 @@ cd ..
|
||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note*: An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
|
!!! Note
|
||||||
|
An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
|
||||||
|
|
||||||
#### 2. Setup your Python virtual environment (virtualenv)
|
#### 2. Setup your Python virtual environment (virtualenv)
|
||||||
|
|
||||||
*Note*: This step is optional but strongly recommended to keep your system organized
|
!!! Note
|
||||||
|
This step is optional but strongly recommended to keep your system organized
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .env
|
python3 -m venv .env
|
||||||
|
@ -337,7 +392,7 @@ cd freqtrade
|
||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
> *To edit the config please refer to [Bot Configuration](configuration.md).*
|
||||||
|
|
||||||
#### 5. Install python dependencies
|
#### 5. Install python dependencies
|
||||||
|
|
||||||
|
@ -396,7 +451,7 @@ copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of inofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_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
|
```cmd
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
|
@ -426,4 +481,4 @@ The easiest way is to download install Microsoft Visual Studio Community [here](
|
||||||
---
|
---
|
||||||
|
|
||||||
Now you have an environment ready, the next step is
|
Now you have an environment ready, the next step is
|
||||||
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
[Bot Configuration](configuration.md).
|
||||||
|
|
52
docs/partials/header.html
Normal file
52
docs/partials/header.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<header class="md-header" data-md-component="header">
|
||||||
|
<nav class="md-header-nav md-grid">
|
||||||
|
<div class="md-flex">
|
||||||
|
<div class="md-flex__cell md-flex__cell--shrink">
|
||||||
|
<a href="{{ config.site_url | default(nav.homepage.url, true) | url }}" title="{{ config.site_name }}"
|
||||||
|
class="md-header-nav__button md-logo">
|
||||||
|
{% if config.theme.logo.icon %}
|
||||||
|
<i class="md-icon">{{ config.theme.logo.icon }}</i>
|
||||||
|
{% else %}
|
||||||
|
<img src="{{ config.theme.logo | url }}" width="24" height="24">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="md-flex__cell md-flex__cell--shrink">
|
||||||
|
<label class="md-icon md-icon--menu md-header-nav__button" for="__drawer"></label>
|
||||||
|
</div>
|
||||||
|
<div class="md-flex__cell md-flex__cell--stretch">
|
||||||
|
<div class="md-flex__ellipsis md-header-nav__title" data-md-component="title">
|
||||||
|
{% block site_name %}
|
||||||
|
{% if config.site_name == page.title %}
|
||||||
|
{{ config.site_name }}
|
||||||
|
{% else %}
|
||||||
|
<span class="md-header-nav__topic">
|
||||||
|
{{ config.site_name }}
|
||||||
|
</span>
|
||||||
|
<span class="md-header-nav__topic">
|
||||||
|
{{ page.title }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-flex__cell md-flex__cell--shrink">
|
||||||
|
{% block search_box %}
|
||||||
|
{% if "search" in config["plugins"] %}
|
||||||
|
<label class="md-icon md-icon--search md-header-nav__button" for="__search"></label>
|
||||||
|
{% include "partials/search.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% if config.repo_url %}
|
||||||
|
<div class="md-flex__cell md-flex__cell--shrink">
|
||||||
|
<div class="md-header-nav__source">
|
||||||
|
{% include "partials/source.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<!-- Place this tag in your head or just before your close body tag. -->
|
||||||
|
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||||
|
</header>
|
|
@ -1,10 +1,6 @@
|
||||||
# Plotting
|
# Plotting
|
||||||
This page explains how to plot prices, indicator, profits.
|
This page explains how to plot prices, indicator, profits.
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
- [Plot price and indicators](#plot-price-and-indicators)
|
|
||||||
- [Plot profit](#plot-profit)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Plotting scripts use Plotly library. Install/upgrade it with:
|
Plotting scripts use Plotly library. Install/upgrade it with:
|
||||||
|
@ -19,7 +15,7 @@ At least version 2.3.0 is required.
|
||||||
Usage for the price plotter:
|
Usage for the price plotter:
|
||||||
|
|
||||||
```
|
```
|
||||||
script/plot_dataframe.py [-h] [-p pair] [--live]
|
script/plot_dataframe.py [-h] [-p pairs] [--live]
|
||||||
```
|
```
|
||||||
|
|
||||||
Example
|
Example
|
||||||
|
@ -27,11 +23,16 @@ Example
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH
|
python scripts/plot_dataframe.py -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p` pair argument, can be used to specify what
|
The `-p` pairs argument, can be used to specify
|
||||||
pair you would like to plot.
|
pairs you would like to plot.
|
||||||
|
|
||||||
**Advanced use**
|
**Advanced use**
|
||||||
|
|
||||||
|
To plot multiple pairs, separate them with a comma:
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
|
||||||
|
```
|
||||||
|
|
||||||
To plot the current live price use the `--live` flag:
|
To plot the current live price use the `--live` flag:
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -p BTC/ETH --live
|
python scripts/plot_dataframe.py -p BTC/ETH --live
|
||||||
|
|
|
@ -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`.**
|
|
||||||
|
|
1
docs/requirements-docs.txt
Normal file
1
docs/requirements-docs.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mkdocs-material==3.1.0
|
|
@ -2,9 +2,20 @@
|
||||||
|
|
||||||
At this stage the bot contains the following stoploss support modes:
|
At this stage the bot contains the following stoploss support modes:
|
||||||
|
|
||||||
1. static stop loss, defined in either the strategy or configuration
|
1. static stop loss, defined in either the strategy or configuration.
|
||||||
2. trailing stop loss, defined in the configuration
|
2. trailing stop loss, defined in the configuration.
|
||||||
3. trailing stop loss, custom positive loss, defined in configuration
|
3. trailing stop loss, custom positive loss, defined in configuration.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values.
|
||||||
|
|
||||||
|
Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
|
||||||
|
|
||||||
|
In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Stoploss on exchange is only supported for Binance as of now.
|
||||||
|
|
||||||
|
|
||||||
## Static Stop Loss
|
## Static Stop Loss
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
This page explains how to command your bot with Telegram.
|
This page explains how to command your bot with Telegram.
|
||||||
|
|
||||||
## Pre-requisite
|
## Prerequisite
|
||||||
To control your bot with Telegram, you need first to
|
To control your bot with Telegram, you need first to
|
||||||
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
[set up a Telegram bot](installation.md)
|
||||||
and add your Telegram API keys into your config file.
|
and add your Telegram API keys into your config file.
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
|
@ -146,4 +146,3 @@ Day Profit BTC Profit USD
|
||||||
## /version
|
## /version
|
||||||
|
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.18.0'
|
__version__ = '0.18.1'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(BaseException):
|
class DependencyException(BaseException):
|
||||||
|
|
|
@ -272,7 +272,7 @@ class Arguments(object):
|
||||||
'-s', '--spaces',
|
'-s', '--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space separate list. \
|
help='Specify which parameters to hyperopt. Space separate list. \
|
||||||
Default: %(default)s',
|
Default: %(default)s',
|
||||||
choices=['all', 'buy', 'roi', 'stoploss'],
|
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
||||||
default='all',
|
default='all',
|
||||||
nargs='+',
|
nargs='+',
|
||||||
dest='spaces',
|
dest='spaces',
|
||||||
|
@ -352,9 +352,9 @@ class Arguments(object):
|
||||||
Parses given arguments for scripts.
|
Parses given arguments for scripts.
|
||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-p', '--pair',
|
'-p', '--pairs',
|
||||||
help='Show profits for only this pairs. Pairs are comma-separated.',
|
help='Show profits for only this pairs. Pairs are comma-separated.',
|
||||||
dest='pair',
|
dest='pairs',
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ from jsonschema import Draft4Validator, validate
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
from freqtrade import OperationalException, constants
|
from freqtrade import OperationalException, constants
|
||||||
|
from freqtrade.state import RunMode
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,9 +35,10 @@ class Configuration(object):
|
||||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Namespace) -> None:
|
def __init__(self, args: Namespace, runmode: RunMode = None) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config: Optional[Dict[str, Any]] = None
|
self.config: Optional[Dict[str, Any]] = None
|
||||||
|
self.runmode = runmode
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
@ -68,6 +70,13 @@ class Configuration(object):
|
||||||
# Load Hyperopt
|
# Load Hyperopt
|
||||||
config = self._load_hyperopt_config(config)
|
config = self._load_hyperopt_config(config)
|
||||||
|
|
||||||
|
# Set runmode
|
||||||
|
if not self.runmode:
|
||||||
|
# Handle real mode, infer dry/live from config
|
||||||
|
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
|
||||||
|
|
||||||
|
config.update({'runmode': self.runmode})
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
||||||
|
@ -124,9 +133,6 @@ class Configuration(object):
|
||||||
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||||
config.update({'db_url': self.args.db_url})
|
config.update({'db_url': self.args.db_url})
|
||||||
logger.info('Parameter --db-url detected ...')
|
logger.info('Parameter --db-url detected ...')
|
||||||
else:
|
|
||||||
# Set default here
|
|
||||||
config.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
|
||||||
|
|
||||||
if config.get('dry_run', False):
|
if config.get('dry_run', False):
|
||||||
logger.info('Dry run is enabled')
|
logger.info('Dry run is enabled')
|
||||||
|
@ -152,13 +158,16 @@ class Configuration(object):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
|
def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str:
|
||||||
|
if not datadir:
|
||||||
|
# set datadir
|
||||||
exchange_name = config.get('exchange', {}).get('name').lower()
|
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||||
default_path = os.path.join('user_data', 'data', exchange_name)
|
datadir = os.path.join('user_data', 'data', exchange_name)
|
||||||
if not os.path.isdir(default_path):
|
|
||||||
os.makedirs(default_path)
|
if not os.path.isdir(datadir):
|
||||||
logger.info(f'Created data directory: {default_path}')
|
os.makedirs(datadir)
|
||||||
return default_path
|
logger.info(f'Created data directory: {datadir}')
|
||||||
|
return datadir
|
||||||
|
|
||||||
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
@ -198,9 +207,9 @@ class Configuration(object):
|
||||||
|
|
||||||
# If --datadir is used we add it to the configuration
|
# If --datadir is used we add it to the configuration
|
||||||
if 'datadir' in self.args and self.args.datadir:
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
config.update({'datadir': self.args.datadir})
|
config.update({'datadir': self._create_datadir(config, self.args.datadir)})
|
||||||
else:
|
else:
|
||||||
config.update({'datadir': self._create_default_datadir(config)})
|
config.update({'datadir': self._create_datadir(config, None)})
|
||||||
logger.info('Using data folder: %s ...', config.get('datadir'))
|
logger.info('Using data folder: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
|
|
|
@ -13,6 +13,7 @@ DEFAULT_HYPEROPT = 'DefaultHyperOpts'
|
||||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
|
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
|
||||||
REQUIRED_ORDERTIF = ['buy', 'sell']
|
REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
|
@ -112,7 +113,8 @@ CONF_SCHEMA = {
|
||||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'}
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
|
'stoploss_on_exchange_interval': {'type': 'number'}
|
||||||
},
|
},
|
||||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,13 +5,19 @@ import logging
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
from freqtrade.constants import TICKER_INTERVAL_MINUTES
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list, ticker_interval: str,
|
||||||
|
fill_missing: bool = True) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
||||||
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
||||||
|
:param ticker_interval: ticker_interval (e.g. 5m). Used to fill up eventual missing data
|
||||||
|
:param fill_missing: fill up missing candles with 0 candles
|
||||||
|
(see ohlcv_fill_up_missing_data for details)
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
logger.debug("Parsing tickerlist to dataframe")
|
logger.debug("Parsing tickerlist to dataframe")
|
||||||
|
@ -23,6 +29,12 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
utc=True,
|
utc=True,
|
||||||
infer_datetime_format=True)
|
infer_datetime_format=True)
|
||||||
|
|
||||||
|
# Some exchanges return int values for volume and even for ohlc.
|
||||||
|
# Convert them since TA-LIB indicators used in the strategy assume floats
|
||||||
|
# and fail with exception...
|
||||||
|
frame = frame.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float',
|
||||||
|
'volume': 'float'})
|
||||||
|
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||||
'open': 'first',
|
'open': 'first',
|
||||||
|
@ -33,9 +45,43 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
})
|
})
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
logger.debug('Dropping last candle')
|
logger.debug('Dropping last candle')
|
||||||
|
|
||||||
|
if fill_missing:
|
||||||
|
return ohlcv_fill_up_missing_data(frame, ticker_interval)
|
||||||
|
else:
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Fills up missing data with 0 volume rows,
|
||||||
|
using the previous close as price for "open", "high" "low" and "close", volume is set to 0
|
||||||
|
|
||||||
|
"""
|
||||||
|
ohlc_dict = {
|
||||||
|
'open': 'first',
|
||||||
|
'high': 'max',
|
||||||
|
'low': 'min',
|
||||||
|
'close': 'last',
|
||||||
|
'volume': 'sum'
|
||||||
|
}
|
||||||
|
tick_mins = TICKER_INTERVAL_MINUTES[ticker_interval]
|
||||||
|
# Resample to create "NAN" values
|
||||||
|
df = dataframe.resample(f'{tick_mins}min', on='date').agg(ohlc_dict)
|
||||||
|
|
||||||
|
# Forwardfill close for missing columns
|
||||||
|
df['close'] = df['close'].fillna(method='ffill')
|
||||||
|
# Use close for "open, high, low"
|
||||||
|
df.loc[:, ['open', 'high', 'low']] = df[['open', 'high', 'low']].fillna(
|
||||||
|
value={'open': df['close'],
|
||||||
|
'high': df['close'],
|
||||||
|
'low': df['close'],
|
||||||
|
})
|
||||||
|
df.reset_index(inplace=True)
|
||||||
|
logger.debug(f"Missing data fillup: before: {len(dataframe)} - after: {len(df)}")
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Gets order book list, returns dataframe with below format per suggested by creslin
|
Gets order book list, returns dataframe with below format per suggested by creslin
|
||||||
|
|
97
freqtrade/data/dataprovider.py
Normal file
97
freqtrade/data/dataprovider.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
"""
|
||||||
|
Dataprovider
|
||||||
|
Responsible to provide data to the bot
|
||||||
|
including Klines, tickers, historic data
|
||||||
|
Common Interface for bot and strategy to access data.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.history import load_pair_history
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DataProvider(object):
|
||||||
|
|
||||||
|
def __init__(self, config: dict, exchange: Exchange) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._exchange = exchange
|
||||||
|
|
||||||
|
def refresh(self,
|
||||||
|
pairlist: List[Tuple[str, str]],
|
||||||
|
helping_pairs: List[Tuple[str, str]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Refresh data, called with each cycle
|
||||||
|
"""
|
||||||
|
if helping_pairs:
|
||||||
|
self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs)
|
||||||
|
else:
|
||||||
|
self._exchange.refresh_latest_ohlcv(pairlist)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_pairs(self) -> List[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
Return a list of tuples containing pair, tick_interval for which data is currently cached.
|
||||||
|
Should be whitelist + open trades.
|
||||||
|
"""
|
||||||
|
return list(self._exchange._klines.keys())
|
||||||
|
|
||||||
|
def ohlcv(self, pair: str, tick_interval: str = None, copy: bool = True) -> DataFrame:
|
||||||
|
"""
|
||||||
|
get ohlcv data for the given pair as DataFrame
|
||||||
|
Please check `available_pairs` to verify which pairs are currently cached.
|
||||||
|
:param pair: pair to get the data for
|
||||||
|
:param tick_interval: ticker_interval to get pair for
|
||||||
|
:param copy: copy dataframe before returning.
|
||||||
|
Use false only for RO operations (where the dataframe is not modified)
|
||||||
|
"""
|
||||||
|
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
|
if tick_interval:
|
||||||
|
pairtick = (pair, tick_interval)
|
||||||
|
else:
|
||||||
|
pairtick = (pair, self._config['ticker_interval'])
|
||||||
|
|
||||||
|
return self._exchange.klines(pairtick, copy=copy)
|
||||||
|
else:
|
||||||
|
return DataFrame()
|
||||||
|
|
||||||
|
def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
get stored historic ohlcv data
|
||||||
|
:param pair: pair to get the data for
|
||||||
|
:param tick_interval: ticker_interval to get pair for
|
||||||
|
"""
|
||||||
|
return load_pair_history(pair=pair,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
refresh_pairs=False,
|
||||||
|
datadir=Path(self._config['datadir']) if self._config.get(
|
||||||
|
'datadir') else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def ticker(self, pair: str):
|
||||||
|
"""
|
||||||
|
Return last ticker data
|
||||||
|
"""
|
||||||
|
# TODO: Implement me
|
||||||
|
pass
|
||||||
|
|
||||||
|
def orderbook(self, pair: str, max: int):
|
||||||
|
"""
|
||||||
|
return latest orderbook data
|
||||||
|
"""
|
||||||
|
# TODO: Implement me
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def runmode(self) -> RunMode:
|
||||||
|
"""
|
||||||
|
Get runmode of the bot
|
||||||
|
can be "live", "dry-run", "backtest", "edgecli", "hyperopt" or "other".
|
||||||
|
"""
|
||||||
|
return RunMode(self._config.get('runmode', RunMode.OTHER))
|
|
@ -5,15 +5,12 @@ includes:
|
||||||
* download data from exchange and store to disk
|
* download data from exchange and store to disk
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import gzip
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Tuple, Any
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
import ujson
|
|
||||||
|
|
||||||
from freqtrade import misc, constants, OperationalException
|
from freqtrade import misc, constants, OperationalException
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
@ -23,15 +20,6 @@ from freqtrade.arguments import TimeRange
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def json_load(data):
|
|
||||||
"""
|
|
||||||
load data with ujson
|
|
||||||
Use this to have a consistent experience,
|
|
||||||
otherwise "precise_float" needs to be passed to all load operations
|
|
||||||
"""
|
|
||||||
return ujson.load(data, precise_float=True)
|
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Trim tickerlist based on given timerange
|
Trim tickerlist based on given timerange
|
||||||
|
@ -77,18 +65,10 @@ def load_tickerdata_file(
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
pair_s = pair.replace('/', '_')
|
pair_s = pair.replace('/', '_')
|
||||||
file = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
file = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||||
gzipfile = file.with_suffix(file.suffix + '.gz')
|
|
||||||
|
|
||||||
# Try gzip file first, otherwise regular json file.
|
pairdata = misc.file_load_json(file)
|
||||||
if gzipfile.is_file():
|
|
||||||
logger.debug('Loading ticker data from file %s', gzipfile)
|
if not pairdata:
|
||||||
with gzip.open(gzipfile) as tickerdata:
|
|
||||||
pairdata = json_load(tickerdata)
|
|
||||||
elif file.is_file():
|
|
||||||
logger.debug('Loading ticker data from file %s', file)
|
|
||||||
with open(file) as tickerdata:
|
|
||||||
pairdata = json_load(tickerdata)
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if timerange:
|
if timerange:
|
||||||
|
@ -102,26 +82,28 @@ def load_pair_history(pair: str,
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||||
refresh_pairs: bool = False,
|
refresh_pairs: bool = False,
|
||||||
exchange: Optional[Exchange] = None,
|
exchange: Optional[Exchange] = None,
|
||||||
|
fill_up_missing: bool = True
|
||||||
) -> DataFrame:
|
) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Loads cached ticker history for the given pair.
|
Loads cached ticker history for the given pair.
|
||||||
:return: DataFrame with ohlcv data
|
:return: DataFrame with ohlcv data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
|
||||||
# If the user force the refresh of pairs
|
# If the user force the refresh of pairs
|
||||||
if refresh_pairs:
|
if refresh_pairs:
|
||||||
if not exchange:
|
if not exchange:
|
||||||
raise OperationalException("Exchange needs to be initialized when "
|
raise OperationalException("Exchange needs to be initialized when "
|
||||||
"calling load_data with refresh_pairs=True")
|
"calling load_data with refresh_pairs=True")
|
||||||
|
|
||||||
logger.info('Download data for all pairs and store them in %s', datadir)
|
logger.info('Download data for pair and store them in %s', datadir)
|
||||||
download_pair_history(datadir=datadir,
|
download_pair_history(datadir=datadir,
|
||||||
exchange=exchange,
|
exchange=exchange,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
tick_interval=ticker_interval,
|
tick_interval=ticker_interval,
|
||||||
timerange=timerange)
|
timerange=timerange)
|
||||||
|
|
||||||
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
|
|
||||||
if pairdata:
|
if pairdata:
|
||||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||||
|
@ -130,7 +112,7 @@ def load_pair_history(pair: str,
|
||||||
logger.warning('Missing data at end for pair %s, data ends at %s',
|
logger.warning('Missing data at end for pair %s, data ends at %s',
|
||||||
pair,
|
pair,
|
||||||
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
return parse_ticker_dataframe(pairdata)
|
return parse_ticker_dataframe(pairdata, ticker_interval, fill_up_missing)
|
||||||
else:
|
else:
|
||||||
logger.warning('No data for pair: "%s", Interval: %s. '
|
logger.warning('No data for pair: "%s", Interval: %s. '
|
||||||
'Use --refresh-pairs-cached to download the data',
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
@ -143,7 +125,8 @@ def load_data(datadir: Optional[Path],
|
||||||
pairs: List[str],
|
pairs: List[str],
|
||||||
refresh_pairs: bool = False,
|
refresh_pairs: bool = False,
|
||||||
exchange: Optional[Exchange] = None,
|
exchange: Optional[Exchange] = None,
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, DataFrame]:
|
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||||
|
fill_up_missing: bool = True) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for a list of pairs the given parameters
|
Loads ticker history data for a list of pairs the given parameters
|
||||||
:return: dict(<pair>:<tickerlist>)
|
:return: dict(<pair>:<tickerlist>)
|
||||||
|
@ -154,7 +137,8 @@ def load_data(datadir: Optional[Path],
|
||||||
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||||
datadir=datadir, timerange=timerange,
|
datadir=datadir, timerange=timerange,
|
||||||
refresh_pairs=refresh_pairs,
|
refresh_pairs=refresh_pairs,
|
||||||
exchange=exchange)
|
exchange=exchange,
|
||||||
|
fill_up_missing=fill_up_missing)
|
||||||
if hist is not None:
|
if hist is not None:
|
||||||
result[pair] = hist
|
result[pair] = hist
|
||||||
return result
|
return result
|
||||||
|
@ -185,7 +169,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str,
|
||||||
# read the cached file
|
# read the cached file
|
||||||
if filename.is_file():
|
if filename.is_file():
|
||||||
with open(filename, "rt") as file:
|
with open(filename, "rt") as file:
|
||||||
data = json_load(file)
|
data = misc.json_load(file)
|
||||||
# remove the last item, could be incomplete candle
|
# remove the last item, could be incomplete candle
|
||||||
if data:
|
if data:
|
||||||
data.pop()
|
data.pop()
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Edge():
|
||||||
|
|
||||||
# checking max_open_trades. it should be -1 as with Edge
|
# checking max_open_trades. it should be -1 as with Edge
|
||||||
# the number of trades is determined by position size
|
# the number of trades is determined by position size
|
||||||
if self.config['max_open_trades'] != -1:
|
if self.config['max_open_trades'] != float('inf'):
|
||||||
logger.critical('max_open_trades should be -1 in config !')
|
logger.critical('max_open_trades should be -1 in config !')
|
||||||
|
|
||||||
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
|
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
|
@ -190,9 +190,16 @@ class Edge():
|
||||||
if self._final_pairs != final:
|
if self._final_pairs != final:
|
||||||
self._final_pairs = final
|
self._final_pairs = final
|
||||||
if self._final_pairs:
|
if self._final_pairs:
|
||||||
logger.info('Edge validated only %s', self._final_pairs)
|
logger.info(
|
||||||
|
'Minimum expectancy and minimum winrate are met only for %s,'
|
||||||
|
' so other pairs are filtered out.',
|
||||||
|
self._final_pairs
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.info('Edge removed all pairs as no pair with minimum expectancy was found !')
|
logger.info(
|
||||||
|
'Edge removed all pairs as no pair with minimum expectancy '
|
||||||
|
'and minimum winrate was found !'
|
||||||
|
)
|
||||||
|
|
||||||
return self._final_pairs
|
return self._final_pairs
|
||||||
|
|
||||||
|
|
|
@ -80,10 +80,10 @@ class Exchange(object):
|
||||||
self._cached_ticker: Dict[str, Any] = {}
|
self._cached_ticker: Dict[str, Any] = {}
|
||||||
|
|
||||||
# Holds last candle refreshed time of each pair
|
# Holds last candle refreshed time of each pair
|
||||||
self._pairs_last_refresh_time: Dict[str, int] = {}
|
self._pairs_last_refresh_time: Dict[Tuple[str, str], int] = {}
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self._klines: Dict[str, DataFrame] = {}
|
self._klines: Dict[Tuple[str, str], DataFrame] = {}
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
self._dry_run_open_orders: Dict[str, Any] = {}
|
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
@ -158,11 +158,12 @@ class Exchange(object):
|
||||||
"""exchange ccxt id"""
|
"""exchange ccxt id"""
|
||||||
return self._api.id
|
return self._api.id
|
||||||
|
|
||||||
def klines(self, pair: str, copy=True) -> DataFrame:
|
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
||||||
if pair in self._klines:
|
# create key tuple
|
||||||
return self._klines[pair].copy() if copy else self._klines[pair]
|
if pair_interval in self._klines:
|
||||||
|
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||||
else:
|
else:
|
||||||
return None
|
return DataFrame()
|
||||||
|
|
||||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||||
if exchange_config.get('sandbox'):
|
if exchange_config.get('sandbox'):
|
||||||
|
@ -236,7 +237,7 @@ class Exchange(object):
|
||||||
f'Exchange {self.name} does not support market orders.')
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
|
||||||
if order_types.get('stoploss_on_exchange'):
|
if order_types.get('stoploss_on_exchange'):
|
||||||
if self.name is not 'Binance':
|
if self.name != 'Binance':
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'On exchange stoploss is not supported for %s.' % self.name
|
'On exchange stoploss is not supported for %s.' % self.name
|
||||||
)
|
)
|
||||||
|
@ -246,7 +247,7 @@ class Exchange(object):
|
||||||
Checks if order time in force configured in strategy/config are supported
|
Checks if order time in force configured in strategy/config are supported
|
||||||
"""
|
"""
|
||||||
if any(v != 'gtc' for k, v in order_time_in_force.items()):
|
if any(v != 'gtc' for k, v in order_time_in_force.items()):
|
||||||
if self.name is not 'Binance':
|
if self.name != 'Binance':
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Time in force policies are not supporetd for {self.name} yet.')
|
f'Time in force policies are not supporetd for {self.name} yet.')
|
||||||
|
|
||||||
|
@ -402,8 +403,11 @@ class Exchange(object):
|
||||||
return self._dry_run_open_orders[order_id]
|
return self._dry_run_open_orders[order_id]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
order = self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||||
amount, rate, {'stopPrice': stop_price})
|
amount, rate, {'stopPrice': stop_price})
|
||||||
|
logger.info('stoploss limit order added for %s. '
|
||||||
|
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
||||||
|
return order
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
|
@ -515,58 +519,68 @@ class Exchange(object):
|
||||||
input_coroutines = [self._async_get_candle_history(
|
input_coroutines = [self._async_get_candle_history(
|
||||||
pair, tick_interval, since) for since in
|
pair, tick_interval, since) for since in
|
||||||
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||||
|
|
||||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||||
|
|
||||||
# Combine tickers
|
# Combine tickers
|
||||||
data: List = []
|
data: List = []
|
||||||
for p, ticker in tickers:
|
for p, ticker_interval, ticker in tickers:
|
||||||
if p == pair:
|
if p == pair:
|
||||||
data.extend(ticker)
|
data.extend(ticker)
|
||||||
# Sort data again after extending the result - above calls return in "async order" order
|
# Sort data again after extending the result - above calls return in "async order"
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info("downloaded %s with length %s.", pair, len(data))
|
logger.info("downloaded %s with length %s.", pair, len(data))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
|
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
|
||||||
"""
|
"""
|
||||||
Refresh tickers asyncronously and set `_klines` of this object with the result
|
Refresh in-memory ohlcv asyncronously and set `_klines` with the result
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing klines for %d pairs", len(pair_list))
|
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
|
||||||
asyncio.get_event_loop().run_until_complete(
|
|
||||||
self.async_get_candles_history(pair_list, ticker_interval))
|
|
||||||
|
|
||||||
async def async_get_candles_history(self, pairs: List[str],
|
|
||||||
tick_interval: str) -> List[Tuple[str, List]]:
|
|
||||||
"""Download ohlcv history for pair-list asyncronously """
|
|
||||||
# Calculating ticker interval in second
|
|
||||||
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
|
|
||||||
input_coroutines = []
|
input_coroutines = []
|
||||||
|
|
||||||
# Gather corotines to run
|
# Gather corotines to run
|
||||||
for pair in pairs:
|
for pair, ticker_interval in set(pair_list):
|
||||||
if not (self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
|
# Calculating ticker interval in second
|
||||||
arrow.utcnow().timestamp and pair in self._klines):
|
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60
|
||||||
input_coroutines.append(self._async_get_candle_history(pair, tick_interval))
|
|
||||||
else:
|
|
||||||
logger.debug("Using cached klines data for %s ...", pair)
|
|
||||||
|
|
||||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
if not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0)
|
||||||
|
+ interval_in_sec) >= arrow.utcnow().timestamp
|
||||||
|
and (pair, ticker_interval) in self._klines):
|
||||||
|
input_coroutines.append(self._async_get_candle_history(pair, ticker_interval))
|
||||||
|
else:
|
||||||
|
logger.debug("Using cached ohlcv data for %s, %s ...", pair, ticker_interval)
|
||||||
|
|
||||||
|
tickers = asyncio.get_event_loop().run_until_complete(
|
||||||
|
asyncio.gather(*input_coroutines, return_exceptions=True))
|
||||||
|
|
||||||
# handle caching
|
# handle caching
|
||||||
for pair, ticks in tickers:
|
for res in tickers:
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||||
|
continue
|
||||||
|
pair = res[0]
|
||||||
|
tick_interval = res[1]
|
||||||
|
ticks = res[2]
|
||||||
# keeping last candle time as last refreshed time of the pair
|
# keeping last candle time as last refreshed time of the pair
|
||||||
if ticks:
|
if ticks:
|
||||||
self._pairs_last_refresh_time[pair] = ticks[-1][0] // 1000
|
self._pairs_last_refresh_time[(pair, tick_interval)] = ticks[-1][0] // 1000
|
||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
self._klines[pair] = parse_ticker_dataframe(ticks)
|
self._klines[(pair, tick_interval)] = parse_ticker_dataframe(
|
||||||
|
ticks, tick_interval, fill_missing=True)
|
||||||
return tickers
|
return tickers
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_get_candle_history(self, pair: str, tick_interval: str,
|
async def _async_get_candle_history(self, pair: str, tick_interval: str,
|
||||||
since_ms: Optional[int] = None) -> Tuple[str, List]:
|
since_ms: Optional[int] = None) -> Tuple[str, str, List]:
|
||||||
|
"""
|
||||||
|
Asyncronously gets candle histories using fetch_ohlcv
|
||||||
|
returns tuple: (pair, tick_interval, ohlcv_list)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# fetch ohlcv asynchronously
|
# fetch ohlcv asynchronously
|
||||||
logger.debug("fetching %s since %s ...", pair, since_ms)
|
logger.debug("fetching %s, %s since %s ...", pair, tick_interval, since_ms)
|
||||||
|
|
||||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
||||||
since=since_ms)
|
since=since_ms)
|
||||||
|
@ -575,11 +589,14 @@ class Exchange(object):
|
||||||
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||||
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||||
# Only sort if necessary to save computing time
|
# Only sort if necessary to save computing time
|
||||||
|
try:
|
||||||
if data and data[0][0] > data[-1][0]:
|
if data and data[0][0] > data[-1][0]:
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
|
except IndexError:
|
||||||
logger.debug("done fetching %s ...", pair)
|
logger.exception("Error loading %s. Result was %s.", pair, data)
|
||||||
return pair, data
|
return pair, tick_interval, []
|
||||||
|
logger.debug("done fetching %s, %s ...", pair, tick_interval)
|
||||||
|
return pair, tick_interval, data
|
||||||
|
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|
|
@ -15,6 +15,7 @@ from requests.exceptions import RequestException
|
||||||
from freqtrade import (DependencyException, OperationalException,
|
from freqtrade import (DependencyException, OperationalException,
|
||||||
TemporaryError, __version__, constants, persistence)
|
TemporaryError, __version__, constants, persistence)
|
||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
@ -34,11 +35,11 @@ class FreqtradeBot(object):
|
||||||
This is from here the bot start its logic.
|
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
|
Init all variables and objects the bot needs to work
|
||||||
:param config: configuration dict, you can use the Configuration.get_config()
|
:param config: configuration dict, you can use Configuration.get_config()
|
||||||
method to get the config dict.
|
to get the config dict.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -54,9 +55,15 @@ class FreqtradeBot(object):
|
||||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
self.wallets = Wallets(self.exchange)
|
self.wallets = Wallets(self.exchange)
|
||||||
|
self.dataprovider = DataProvider(self.config, self.exchange)
|
||||||
|
|
||||||
|
# Attach Dataprovider to Strategy baseclass
|
||||||
|
IStrategy.dp = self.dataprovider
|
||||||
|
# Attach Wallets to Strategy baseclass
|
||||||
|
IStrategy.wallets = self.wallets
|
||||||
|
|
||||||
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList')
|
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList')
|
||||||
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
|
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
|
||||||
|
|
||||||
|
@ -151,9 +158,6 @@ class FreqtradeBot(object):
|
||||||
self.active_pair_whitelist = self.pairlists.whitelist
|
self.active_pair_whitelist = self.pairlists.whitelist
|
||||||
|
|
||||||
# Calculating Edge positiong
|
# Calculating Edge positiong
|
||||||
# Should be called before refresh_tickers
|
|
||||||
# Otherwise it will override cached klines in exchange
|
|
||||||
# with delta value (klines only from last refresh_pairs)
|
|
||||||
if self.edge:
|
if self.edge:
|
||||||
self.edge.calculate()
|
self.edge.calculate()
|
||||||
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
|
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
|
||||||
|
@ -166,8 +170,12 @@ class FreqtradeBot(object):
|
||||||
self.active_pair_whitelist.extend([trade.pair for trade in trades
|
self.active_pair_whitelist.extend([trade.pair for trade in trades
|
||||||
if trade.pair not in self.active_pair_whitelist])
|
if trade.pair not in self.active_pair_whitelist])
|
||||||
|
|
||||||
|
# Create pair-whitelist tuple with (pair, ticker_interval)
|
||||||
|
pair_whitelist_tuple = [(pair, self.config['ticker_interval'])
|
||||||
|
for pair in self.active_pair_whitelist]
|
||||||
# Refreshing candles
|
# Refreshing candles
|
||||||
self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval)
|
self.dataprovider.refresh(pair_whitelist_tuple,
|
||||||
|
self.strategy.informative_pairs())
|
||||||
|
|
||||||
# First process current opened trades
|
# First process current opened trades
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
@ -183,7 +191,7 @@ class FreqtradeBot(object):
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
except TemporaryError as error:
|
except TemporaryError as error:
|
||||||
logger.warning('%s, retrying in 30 seconds...', error)
|
logger.warning(f"Error: {error}, retrying in {constants.RETRY_TIMEOUT} seconds...")
|
||||||
time.sleep(constants.RETRY_TIMEOUT)
|
time.sleep(constants.RETRY_TIMEOUT)
|
||||||
except OperationalException:
|
except OperationalException:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
|
@ -196,19 +204,11 @@ class FreqtradeBot(object):
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
|
def get_target_bid(self, pair: str) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid target between current ask price and last price
|
Calculates bid target between current ask price and last price
|
||||||
:param ticker: Ticker to use for getting Ask and Last Price
|
|
||||||
:return: float: Price
|
:return: float: Price
|
||||||
"""
|
"""
|
||||||
if ticker['ask'] < ticker['last']:
|
|
||||||
ticker_rate = ticker['ask']
|
|
||||||
else:
|
|
||||||
balance = self.config['bid_strategy']['ask_last_balance']
|
|
||||||
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
|
||||||
|
|
||||||
used_rate = ticker_rate
|
|
||||||
config_bid_strategy = self.config.get('bid_strategy', {})
|
config_bid_strategy = self.config.get('bid_strategy', {})
|
||||||
if 'use_order_book' in config_bid_strategy and\
|
if 'use_order_book' in config_bid_strategy and\
|
||||||
config_bid_strategy.get('use_order_book', False):
|
config_bid_strategy.get('use_order_book', False):
|
||||||
|
@ -218,15 +218,16 @@ class FreqtradeBot(object):
|
||||||
logger.debug('order_book %s', order_book)
|
logger.debug('order_book %s', order_book)
|
||||||
# top 1 = index 0
|
# top 1 = index 0
|
||||||
order_book_rate = order_book['bids'][order_book_top - 1][0]
|
order_book_rate = order_book['bids'][order_book_top - 1][0]
|
||||||
# if ticker has lower rate, then use ticker ( usefull if down trending )
|
|
||||||
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
|
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
|
||||||
if ticker_rate < order_book_rate:
|
|
||||||
logger.info('...using ticker rate instead %0.8f', ticker_rate)
|
|
||||||
used_rate = ticker_rate
|
|
||||||
else:
|
|
||||||
used_rate = order_book_rate
|
used_rate = order_book_rate
|
||||||
else:
|
else:
|
||||||
logger.info('Using Last Ask / Last Price')
|
logger.info('Using Last Ask / Last Price')
|
||||||
|
ticker = self.exchange.get_ticker(pair)
|
||||||
|
if ticker['ask'] < ticker['last']:
|
||||||
|
ticker_rate = ticker['ask']
|
||||||
|
else:
|
||||||
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
|
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
used_rate = ticker_rate
|
used_rate = ticker_rate
|
||||||
|
|
||||||
return used_rate
|
return used_rate
|
||||||
|
@ -259,9 +260,8 @@ class FreqtradeBot(object):
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if avaliable_amount < stake_amount:
|
if avaliable_amount < stake_amount:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Available balance(%f %s) is lower than stake amount(%f %s)' % (
|
f"Available balance({avaliable_amount} {self.config['stake_currency']}) is "
|
||||||
avaliable_amount, self.config['stake_currency'],
|
f"lower than stake amount({stake_amount} {self.config['stake_currency']})"
|
||||||
stake_amount, self.config['stake_currency'])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
@ -290,7 +290,9 @@ class FreqtradeBot(object):
|
||||||
if not min_stake_amounts:
|
if not min_stake_amounts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
|
# reserve some percent defined in config (5% default) + stoploss
|
||||||
|
amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent',
|
||||||
|
constants.DEFAULT_AMOUNT_RESERVE_PERCENT)
|
||||||
if self.strategy.stoploss is not None:
|
if self.strategy.stoploss is not None:
|
||||||
amount_reserve_percent += self.strategy.stoploss
|
amount_reserve_percent += self.strategy.stoploss
|
||||||
# it should not be more than 50%
|
# it should not be more than 50%
|
||||||
|
@ -317,16 +319,16 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines(_pair))
|
(buy, sell) = self.strategy.get_signal(
|
||||||
|
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
stake_amount = self._get_trade_stake_amount(_pair)
|
stake_amount = self._get_trade_stake_amount(_pair)
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(
|
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
||||||
'Buy signal found: about create a new trade with stake_amount: %f ...',
|
f"{stake_amount} ...")
|
||||||
stake_amount
|
|
||||||
)
|
|
||||||
|
|
||||||
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
||||||
get('check_depth_of_market', {})
|
get('check_depth_of_market', {})
|
||||||
|
@ -373,13 +375,13 @@ class FreqtradeBot(object):
|
||||||
buy_limit_requested = price
|
buy_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit_requested = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
buy_limit_requested = self.get_target_bid(pair)
|
||||||
|
|
||||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
|
||||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Can\'t open a new trade for {pair_s}: stake amount'
|
f'Can\'t open a new trade for {pair_s}: stake amount '
|
||||||
f' is too small ({stake_amount} < {min_stake_amount})'
|
f'is too small ({stake_amount} < {min_stake_amount})'
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -497,7 +499,7 @@ class FreqtradeBot(object):
|
||||||
trade.fee_open = 0
|
trade.fee_open = 0
|
||||||
|
|
||||||
except OperationalException as exception:
|
except OperationalException as exception:
|
||||||
logger.warning("could not update trade amount: %s", exception)
|
logger.warning("Could not update trade amount: %s", exception)
|
||||||
|
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
|
|
||||||
|
@ -556,12 +558,12 @@ class FreqtradeBot(object):
|
||||||
fee_abs += exectrade['fee']['cost']
|
fee_abs += exectrade['fee']['cost']
|
||||||
|
|
||||||
if amount != order_amount:
|
if amount != order_amount:
|
||||||
logger.warning(f"amount {amount} does not match amount {trade.amount}")
|
logger.warning(f"Amount {amount} does not match amount {trade.amount}")
|
||||||
raise OperationalException("Half bought? Amounts don't match")
|
raise OperationalException("Half bought? Amounts don't match")
|
||||||
real_amount = amount - fee_abs
|
real_amount = amount - fee_abs
|
||||||
if fee_abs != 0:
|
if fee_abs != 0:
|
||||||
logger.info(f"""Applying fee on amount for {trade} \
|
logger.info(f"Applying fee on amount for {trade} "
|
||||||
(from {order_amount} to {real_amount}) from Trades""")
|
f"(from {order_amount} to {real_amount}) from Trades")
|
||||||
return real_amount
|
return real_amount
|
||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
|
@ -570,16 +572,16 @@ class FreqtradeBot(object):
|
||||||
:return: True if trade has been sold, False otherwise
|
:return: True if trade has been sold, False otherwise
|
||||||
"""
|
"""
|
||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
raise ValueError(f'attempt to handle closed trade: {trade}')
|
raise ValueError(f'Attempt to handle closed trade: {trade}')
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
|
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
(buy, sell) = self.strategy.get_signal(
|
||||||
self.exchange.klines(trade.pair))
|
trade.pair, self.strategy.ticker_interval,
|
||||||
|
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
||||||
|
|
||||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
if config_ask_strategy.get('use_order_book', False):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
|
@ -592,18 +594,15 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
for i in range(order_book_min, order_book_max + 1):
|
for i in range(order_book_min, order_book_max + 1):
|
||||||
order_book_rate = order_book['asks'][i - 1][0]
|
order_book_rate = order_book['asks'][i - 1][0]
|
||||||
|
|
||||||
# if orderbook has higher rate (high profit),
|
|
||||||
# use orderbook, otherwise just use bids rate
|
|
||||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||||
if sell_rate < order_book_rate:
|
|
||||||
sell_rate = order_book_rate
|
sell_rate = order_book_rate
|
||||||
|
|
||||||
if self.check_sell(trade, sell_rate, buy, sell):
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
|
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||||
if self.check_sell(trade, sell_rate, buy, sell):
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -613,7 +612,7 @@ class FreqtradeBot(object):
|
||||||
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if trade is fulfilled in which case the stoploss
|
Check if trade is fulfilled in which case the stoploss
|
||||||
on exchange should be added immediately if stoploss on exchnage
|
on exchange should be added immediately if stoploss on exchange
|
||||||
is enabled.
|
is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -630,13 +629,14 @@ class FreqtradeBot(object):
|
||||||
stop_price = trade.open_rate * (1 + stoploss)
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
# limit price should be less than stop price.
|
# limit price should be less than stop price.
|
||||||
# 0.98 is arbitrary here.
|
# 0.99 is arbitrary here.
|
||||||
limit_price = stop_price * 0.98
|
limit_price = stop_price * 0.99
|
||||||
|
|
||||||
stoploss_order_id = self.exchange.stoploss_limit(
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||||
)['id']
|
)['id']
|
||||||
trade.stoploss_order_id = str(stoploss_order_id)
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
trade.stoploss_last_update = datetime.now()
|
||||||
|
|
||||||
# Or the trade open and there is already a stoploss on exchange.
|
# Or the trade open and there is already a stoploss on exchange.
|
||||||
# so we check if it is hit ...
|
# so we check if it is hit ...
|
||||||
|
@ -647,10 +647,38 @@ class FreqtradeBot(object):
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
result = True
|
result = True
|
||||||
else:
|
elif self.config.get('trailing_stop', False):
|
||||||
result = False
|
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||||
|
# in which case we cancel stoploss order and put another one with new
|
||||||
|
# value immediately
|
||||||
|
self.handle_trailing_stoploss_on_exchange(trade, order)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
|
||||||
|
"""
|
||||||
|
Check to see if stoploss on exchange should be updated
|
||||||
|
in case of trailing stoploss on exchange
|
||||||
|
:param Trade: Corresponding Trade
|
||||||
|
:param order: Current on exchange stoploss order
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if trade.stop_loss > float(order['info']['stopPrice']):
|
||||||
|
# we check if the update is neccesary
|
||||||
|
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||||
|
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat:
|
||||||
|
# cancelling the current stoploss on exchange first
|
||||||
|
logger.info('Trailing stoploss: cancelling current stoploss on exchange '
|
||||||
|
'in order to add another one ...')
|
||||||
|
if self.exchange.cancel_order(order['id'], trade.pair):
|
||||||
|
# creating the new one
|
||||||
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
|
pair=trade.pair, amount=trade.amount,
|
||||||
|
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||||
|
)['id']
|
||||||
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
|
||||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
if self.edge:
|
if self.edge:
|
||||||
stoploss = self.edge.stoploss(trade.pair)
|
stoploss = self.edge.stoploss(trade.pair)
|
||||||
|
@ -698,8 +726,15 @@ class FreqtradeBot(object):
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if trade is still actually open
|
# Handle cancelled on exchange
|
||||||
if order['status'] == 'open':
|
if order['status'] == 'canceled':
|
||||||
|
if order['side'] == 'buy':
|
||||||
|
self.handle_buy_order_full_cancel(trade, "canceled on Exchange")
|
||||||
|
elif order['side'] == 'sell':
|
||||||
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
self.wallets.update()
|
||||||
|
# Check if order is still actually open
|
||||||
|
elif order['status'] == 'open':
|
||||||
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||||
self.handle_timedout_limit_buy(trade, order)
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
@ -707,24 +742,24 @@ class FreqtradeBot(object):
|
||||||
self.handle_timedout_limit_sell(trade, order)
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
|
||||||
# it is conditionally called in the
|
"""Close trade in database and send message"""
|
||||||
# handle_timedout_limit_sell()?
|
Trade.session.delete(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
logger.info('Buy order %s for %s.', reason, trade)
|
||||||
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Unfilled buy order for {trade.pair} {reason}'
|
||||||
|
})
|
||||||
|
|
||||||
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
||||||
"""Buy timeout - cancel order
|
"""Buy timeout - cancel order
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
pair_s = trade.pair.replace('_', '/')
|
|
||||||
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
if order['remaining'] == order['amount']:
|
if order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just delete the trade
|
# if trade is not partially completed, just delete the trade
|
||||||
Trade.session.delete(trade)
|
self.handle_buy_order_full_cancel(trade, "cancelled due to timeout")
|
||||||
Trade.session.flush()
|
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
|
||||||
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
|
|
||||||
})
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if trade is partially complete, edit the stake details for the trade
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
|
@ -735,20 +770,24 @@ class FreqtradeBot(object):
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
logger.info('Partial buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg({
|
self.rpc.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
'status': f'Remaining buy order for {pair_s} cancelled due to timeout'
|
'status': f'Remaining buy order for {trade.pair} cancelled due to timeout'
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
|
||||||
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
|
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Sell timeout - cancel order and update trade
|
Sell timeout - cancel order and update trade
|
||||||
:return: True if order was fully cancelled
|
:return: True if order was fully cancelled
|
||||||
"""
|
"""
|
||||||
pair_s = trade.pair.replace('_', '/')
|
|
||||||
if order['remaining'] == order['amount']:
|
if order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just cancel the trade
|
# if trade is not partially completed, just cancel the trade
|
||||||
|
if order["status"] != "canceled":
|
||||||
|
reason = "due to timeout"
|
||||||
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
|
else:
|
||||||
|
reason = "on exchange"
|
||||||
|
logger.info('Sell order canceled on exchange for %s.', trade)
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_profit = None
|
trade.close_profit = None
|
||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
|
@ -756,9 +795,9 @@ class FreqtradeBot(object):
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
self.rpc.send_msg({
|
self.rpc.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout'
|
'status': f'Unfilled sell order for {trade.pair} cancelled {reason}'
|
||||||
})
|
})
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
|
|
|
@ -25,7 +25,7 @@ def main(sysargv: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
arguments = Arguments(
|
arguments = Arguments(
|
||||||
sysargv,
|
sysargv,
|
||||||
'Simple High Frequency Trading Bot for crypto currencies'
|
'Free, open source crypto trading bot'
|
||||||
)
|
)
|
||||||
args = arguments.get_parsed_arg()
|
args = arguments.get_parsed_arg()
|
||||||
|
|
||||||
|
@ -39,13 +39,13 @@ def main(sysargv: List[str]) -> None:
|
||||||
return_code = 1
|
return_code = 1
|
||||||
try:
|
try:
|
||||||
# Load and validate configuration
|
# Load and validate configuration
|
||||||
config = Configuration(args).get_config()
|
config = Configuration(args, None).get_config()
|
||||||
|
|
||||||
# Init the bot
|
# Init the bot
|
||||||
freqtrade = FreqtradeBot(config)
|
freqtrade = FreqtradeBot(config)
|
||||||
|
|
||||||
state = None
|
state = None
|
||||||
while 1:
|
while True:
|
||||||
state = freqtrade.worker(old_state=state)
|
state = freqtrade.worker(old_state=state)
|
||||||
if state == State.RELOAD_CONF:
|
if state == State.RELOAD_CONF:
|
||||||
freqtrade = reconfigure(freqtrade, args)
|
freqtrade = reconfigure(freqtrade, args)
|
||||||
|
@ -76,7 +76,7 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
|
||||||
freqtrade.cleanup()
|
freqtrade.cleanup()
|
||||||
|
|
||||||
# Create new instance
|
# Create new instance
|
||||||
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
freqtrade = FreqtradeBot(Configuration(args, None).get_config())
|
||||||
freqtrade.rpc.send_msg({
|
freqtrade.rpc.send_msg({
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
'status': 'config reloaded'
|
'status': 'config reloaded'
|
||||||
|
|
|
@ -3,7 +3,6 @@ Various tool function for Freqtrade and scripts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -11,6 +10,7 @@ from typing import Dict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
import rapidjson
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -38,12 +38,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||||
An numpy-array of datetimes
|
An numpy-array of datetimes
|
||||||
:return: numpy-array of datetime
|
:return: numpy-array of datetime
|
||||||
"""
|
"""
|
||||||
times = []
|
return dates.dt.to_pydatetime()
|
||||||
dates = dates.astype(datetime)
|
|
||||||
for index in range(0, dates.size):
|
|
||||||
date = dates[index].to_pydatetime()
|
|
||||||
times.append(date)
|
|
||||||
return np.array(times)
|
|
||||||
|
|
||||||
|
|
||||||
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
||||||
|
@ -71,16 +66,45 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
||||||
:param data: JSON Data to save
|
:param data: JSON Data to save
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
print(f'dumping json to "{filename}"')
|
logger.info(f'dumping json to "{filename}"')
|
||||||
|
|
||||||
if is_zip:
|
if is_zip:
|
||||||
if not filename.endswith('.gz'):
|
if not filename.endswith('.gz'):
|
||||||
filename = filename + '.gz'
|
filename = filename + '.gz'
|
||||||
with gzip.open(filename, 'w') as fp:
|
with gzip.open(filename, 'w') as fp:
|
||||||
json.dump(data, fp, default=str)
|
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||||
else:
|
else:
|
||||||
with open(filename, 'w') as fp:
|
with open(filename, 'w') as fp:
|
||||||
json.dump(data, fp, default=str)
|
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
|
logger.debug(f'done json to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
|
def json_load(datafile):
|
||||||
|
"""
|
||||||
|
load data with rapidjson
|
||||||
|
Use this to have a consistent experience,
|
||||||
|
sete number_mode to "NM_NATIVE" for greatest speed
|
||||||
|
"""
|
||||||
|
return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE)
|
||||||
|
|
||||||
|
|
||||||
|
def file_load_json(file):
|
||||||
|
|
||||||
|
gzipfile = file.with_suffix(file.suffix + '.gz')
|
||||||
|
|
||||||
|
# Try gzip file first, otherwise regular json file.
|
||||||
|
if gzipfile.is_file():
|
||||||
|
logger.debug('Loading ticker data from file %s', gzipfile)
|
||||||
|
with gzip.open(gzipfile) as tickerdata:
|
||||||
|
pairdata = json_load(tickerdata)
|
||||||
|
elif file.is_file():
|
||||||
|
logger.debug('Loading ticker data from file %s', file)
|
||||||
|
with open(file) as tickerdata:
|
||||||
|
pairdata = json_load(tickerdata)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
def format_ms_time(date: int) -> str:
|
def format_ms_time(date: int) -> str:
|
||||||
|
|
|
@ -13,7 +13,7 @@ from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
from freqtrade import optimize
|
||||||
from freqtrade import DependencyException, constants
|
from freqtrade import DependencyException, constants
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
|
@ -22,6 +22,7 @@ from freqtrade.data import history
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import SellType, IStrategy
|
from freqtrade.strategy.interface import SellType, IStrategy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -100,11 +101,13 @@ class Backtesting(object):
|
||||||
:return: pretty printed table with tabulate as str
|
:return: pretty printed table with tabulate as str
|
||||||
"""
|
"""
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
max_open_trades = self.config.get('max_open_trades')
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
|
||||||
|
'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results.pair == pair]
|
||||||
if skip_nan and result.profit_abs.isnull().all():
|
if skip_nan and result.profit_abs.isnull().all():
|
||||||
|
@ -116,6 +119,7 @@ class Backtesting(object):
|
||||||
result.profit_percent.mean() * 100.0,
|
result.profit_percent.mean() * 100.0,
|
||||||
result.profit_percent.sum() * 100.0,
|
result.profit_percent.sum() * 100.0,
|
||||||
result.profit_abs.sum(),
|
result.profit_abs.sum(),
|
||||||
|
result.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
str(timedelta(
|
str(timedelta(
|
||||||
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||||
len(result[result.profit_abs > 0]),
|
len(result[result.profit_abs > 0]),
|
||||||
|
@ -129,12 +133,15 @@ class Backtesting(object):
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_percent.sum() * 100.0,
|
results.profit_percent.sum() * 100.0,
|
||||||
results.profit_abs.sum(),
|
results.profit_abs.sum(),
|
||||||
|
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
str(timedelta(
|
str(timedelta(
|
||||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
len(results[results.profit_abs > 0]),
|
len(results[results.profit_abs > 0]),
|
||||||
len(results[results.profit_abs < 0])
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -151,11 +158,13 @@ class Backtesting(object):
|
||||||
Generate summary table per strategy
|
Generate summary table per strategy
|
||||||
"""
|
"""
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
max_open_trades = self.config.get('max_open_trades')
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'tot profit ' + stake_currency, 'tot profit %', 'avg duration',
|
||||||
|
'profit', 'loss']
|
||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
strategy,
|
strategy,
|
||||||
|
@ -163,12 +172,15 @@ class Backtesting(object):
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_percent.sum() * 100.0,
|
results.profit_percent.sum() * 100.0,
|
||||||
results.profit_abs.sum(),
|
results.profit_abs.sum(),
|
||||||
|
results.profit_percent.sum() * 100.0 / max_open_trades,
|
||||||
str(timedelta(
|
str(timedelta(
|
||||||
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
len(results[results.profit_abs > 0]),
|
len(results[results.profit_abs > 0]),
|
||||||
len(results[results.profit_abs < 0])
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
||||||
strategyname: Optional[str] = None) -> None:
|
strategyname: Optional[str] = None) -> None:
|
||||||
|
@ -219,7 +231,8 @@ class Backtesting(object):
|
||||||
# Set close_rate to stoploss
|
# Set close_rate to stoploss
|
||||||
closerate = trade.stop_loss
|
closerate = trade.stop_loss
|
||||||
elif sell.sell_type == (SellType.ROI):
|
elif sell.sell_type == (SellType.ROI):
|
||||||
# get entry in min_roi >= to trade duration
|
# get next entry in min_roi > to trade duration
|
||||||
|
# Interface.py skips on trade_duration <= duration
|
||||||
roi_entry = max(list(filter(lambda x: trade_dur >= x,
|
roi_entry = max(list(filter(lambda x: trade_dur >= x,
|
||||||
self.strategy.minimal_roi.keys())))
|
self.strategy.minimal_roi.keys())))
|
||||||
roi = self.strategy.minimal_roi[roi_entry]
|
roi = self.strategy.minimal_roi[roi_entry]
|
||||||
|
@ -362,8 +375,9 @@ class Backtesting(object):
|
||||||
|
|
||||||
if self.config.get('live'):
|
if self.config.get('live'):
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
self.exchange.refresh_tickers(pairs, self.ticker_interval)
|
self.exchange.refresh_latest_ohlcv([(pair, self.ticker_interval) for pair in pairs])
|
||||||
data = self.exchange._klines
|
data = {key[0]: value for key, value in self.exchange._klines.items()}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
|
@ -393,12 +407,9 @@ class Backtesting(object):
|
||||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||||
self._set_strategy(strat)
|
self._set_strategy(strat)
|
||||||
|
|
||||||
# need to reprocess data every time to populate signals
|
min_date, max_date = optimize.get_timeframe(data)
|
||||||
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
# Validate dataframe for missing values (mainly at start and end, as fillup is called)
|
||||||
|
optimize.validate_backtest_data(data, min_date, max_date,
|
||||||
min_date, max_date = optimize.get_timeframe(preprocessed)
|
|
||||||
# Validate dataframe for missing values
|
|
||||||
optimize.validate_backtest_data(preprocessed, min_date, max_date,
|
|
||||||
constants.TICKER_INTERVAL_MINUTES[self.ticker_interval])
|
constants.TICKER_INTERVAL_MINUTES[self.ticker_interval])
|
||||||
logger.info(
|
logger.info(
|
||||||
'Measuring data from %s up to %s (%s days)..',
|
'Measuring data from %s up to %s (%s days)..',
|
||||||
|
@ -406,6 +417,8 @@ class Backtesting(object):
|
||||||
max_date.isoformat(),
|
max_date.isoformat(),
|
||||||
(max_date - min_date).days
|
(max_date - min_date).days
|
||||||
)
|
)
|
||||||
|
# need to reprocess data every time to populate signals
|
||||||
|
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
||||||
|
@ -426,18 +439,18 @@ class Backtesting(object):
|
||||||
strategy if len(self.strategylist) > 1 else None)
|
strategy if len(self.strategylist) > 1 else None)
|
||||||
|
|
||||||
print(f"Result for strategy {strategy}")
|
print(f"Result for strategy {strategy}")
|
||||||
print(' BACKTESTING REPORT '.center(119, '='))
|
print(' BACKTESTING REPORT '.center(133, '='))
|
||||||
print(self._generate_text_table(data, results))
|
print(self._generate_text_table(data, results))
|
||||||
|
|
||||||
print(' SELL REASON STATS '.center(119, '='))
|
print(' SELL REASON STATS '.center(133, '='))
|
||||||
print(self._generate_text_table_sell_reason(data, results))
|
print(self._generate_text_table_sell_reason(data, results))
|
||||||
|
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
|
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||||
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
|
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
|
||||||
print()
|
print()
|
||||||
if len(all_results) > 1:
|
if len(all_results) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
print(' Strategy Summary '.center(119, '='))
|
print(' Strategy Summary '.center(133, '='))
|
||||||
print(self._generate_text_table_strategy(all_results))
|
print(self._generate_text_table_strategy(all_results))
|
||||||
print('\nFor more details, please look at the detail tables above')
|
print('\nFor more details, please look at the detail tables above')
|
||||||
|
|
||||||
|
@ -448,7 +461,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: Configuration
|
:return: Configuration
|
||||||
"""
|
"""
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args, RunMode.BACKTEST)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
|
|
|
@ -33,6 +33,7 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
# Bollinger bands
|
# Bollinger bands
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||||
|
|
||||||
# TRIGGERS
|
# TRIGGERS
|
||||||
|
if 'trigger' in params:
|
||||||
if params['trigger'] == 'bb_lower':
|
if params['trigger'] == 'bb_lower':
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
|
@ -93,6 +95,67 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Define the sell strategy parameters to be used by hyperopt
|
||||||
|
"""
|
||||||
|
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Sell strategy Hyperopt will build and use
|
||||||
|
"""
|
||||||
|
# print(params)
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||||
|
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||||
|
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
||||||
|
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
||||||
|
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
||||||
|
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
||||||
|
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
||||||
|
|
||||||
|
# TRIGGERS
|
||||||
|
if 'sell-trigger' in params:
|
||||||
|
if params['sell-trigger'] == 'sell-bb_upper':
|
||||||
|
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
||||||
|
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['macdsignal'], dataframe['macd']
|
||||||
|
))
|
||||||
|
if params['sell-trigger'] == 'sell-sar_reversal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['sar'], dataframe['close']
|
||||||
|
))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
return populate_sell_trend
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Define your Hyperopt space for searching sell strategy parameters
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(75, 100, name='sell-mfi-value'),
|
||||||
|
Integer(50, 100, name='sell-fastd-value'),
|
||||||
|
Integer(50, 100, name='sell-adx-value'),
|
||||||
|
Integer(60, 100, name='sell-rsi-value'),
|
||||||
|
Categorical([True, False], name='sell-mfi-enabled'),
|
||||||
|
Categorical([True, False], name='sell-fastd-enabled'),
|
||||||
|
Categorical([True, False], name='sell-adx-enabled'),
|
||||||
|
Categorical([True, False], name='sell-rsi-enabled'),
|
||||||
|
Categorical(['sell-bb_upper',
|
||||||
|
'sell-macd_cross_signal',
|
||||||
|
'sell-sar_reversal'], name='sell-trigger')
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
|
@ -128,3 +191,36 @@ class DefaultHyperOpts(IHyperOpt):
|
||||||
Real(0.01, 0.07, name='roi_p2'),
|
Real(0.01, 0.07, name='roi_p2'),
|
||||||
Real(0.01, 0.20, name='roi_p3'),
|
Real(0.01, 0.20, name='roi_p3'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators. Should be a copy of from strategy
|
||||||
|
must align to populate_indicators in this file
|
||||||
|
Only used when --spaces does not include buy
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['close'] < dataframe['bb_lowerband']) &
|
||||||
|
(dataframe['mfi'] < 16) &
|
||||||
|
(dataframe['adx'] > 25) &
|
||||||
|
(dataframe['rsi'] < 21)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators. Should be a copy of from strategy
|
||||||
|
must align to populate_indicators in this file
|
||||||
|
Only used when --spaces does not include sell
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(
|
||||||
|
dataframe['macdsignal'], dataframe['macd']
|
||||||
|
)) &
|
||||||
|
(dataframe['fastd'] > 54)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
||||||
|
|
|
@ -9,10 +9,11 @@ from typing import Dict, Any
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
|
|
||||||
from freqtrade.configuration import Configuration
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -67,7 +68,9 @@ class EdgeCli(object):
|
||||||
round(result[1].avg_trade_duration)
|
round(result[1].avg_trade_duration)
|
||||||
])
|
])
|
||||||
|
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
# Ignore type as floatfmt does allow tuples but mypy does not know that
|
||||||
|
return tabulate(tabular_data, headers=headers, # type: ignore
|
||||||
|
floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.edge.calculate()
|
self.edge.calculate()
|
||||||
|
@ -81,7 +84,7 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
:param args: Cli args from Arguments()
|
:param args: Cli args from Arguments()
|
||||||
:return: Configuration
|
:return: Configuration
|
||||||
"""
|
"""
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args, RunMode.EDGECLI)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
|
|
|
@ -5,17 +5,18 @@ This module contains the hyperopt logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from argparse import Namespace
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from argparse import Namespace
|
||||||
from math import exp
|
from math import exp
|
||||||
import multiprocessing
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from pathlib import Path
|
||||||
|
from pprint import pprint
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from pandas import DataFrame
|
|
||||||
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects
|
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects
|
||||||
|
from pandas import DataFrame
|
||||||
from skopt import Optimizer
|
from skopt import Optimizer
|
||||||
from skopt.space import Dimension
|
from skopt.space import Dimension
|
||||||
|
|
||||||
|
@ -24,9 +25,9 @@ from freqtrade.configuration import Configuration
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.optimize import get_timeframe
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.resolvers import HyperOptResolver
|
from freqtrade.resolvers import HyperOptResolver
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||||
|
@ -102,13 +103,13 @@ class Hyperopt(Backtesting):
|
||||||
results = sorted(self.trials, key=itemgetter('loss'))
|
results = sorted(self.trials, key=itemgetter('loss'))
|
||||||
best_result = results[0]
|
best_result = results[0]
|
||||||
logger.info(
|
logger.info(
|
||||||
'Best result:\n%s\nwith values:\n%s',
|
'Best result:\n%s\nwith values:\n',
|
||||||
best_result['result'],
|
best_result['result']
|
||||||
best_result['params']
|
|
||||||
)
|
)
|
||||||
|
pprint(best_result['params'], indent=4)
|
||||||
if 'roi_t1' in best_result['params']:
|
if 'roi_t1' in best_result['params']:
|
||||||
logger.info('ROI table:\n%s',
|
logger.info('ROI table:')
|
||||||
self.custom_hyperopt.generate_roi_table(best_result['params']))
|
pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4)
|
||||||
|
|
||||||
def log_results(self, results) -> None:
|
def log_results(self, results) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -151,6 +152,12 @@ class Hyperopt(Backtesting):
|
||||||
spaces: List[Dimension] = []
|
spaces: List[Dimension] = []
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
spaces += self.custom_hyperopt.indicator_space()
|
spaces += self.custom_hyperopt.indicator_space()
|
||||||
|
if self.has_space('sell'):
|
||||||
|
spaces += self.custom_hyperopt.sell_indicator_space()
|
||||||
|
# Make sure experimental is enabled
|
||||||
|
if 'experimental' not in self.config:
|
||||||
|
self.config['experimental'] = {}
|
||||||
|
self.config['experimental']['use_sell_signal'] = True
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
spaces += self.custom_hyperopt.roi_space()
|
spaces += self.custom_hyperopt.roi_space()
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
|
@ -164,6 +171,13 @@ class Hyperopt(Backtesting):
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
||||||
|
elif hasattr(self.custom_hyperopt, 'populate_buy_trend'):
|
||||||
|
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
|
||||||
|
|
||||||
|
if self.has_space('sell'):
|
||||||
|
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
|
||||||
|
elif hasattr(self.custom_hyperopt, 'populate_sell_trend'):
|
||||||
|
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
|
||||||
|
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
self.strategy.stoploss = params['stoploss']
|
self.strategy.stoploss = params['stoploss']
|
||||||
|
@ -247,7 +261,7 @@ class Hyperopt(Backtesting):
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy') or self.has_space('sell'):
|
||||||
self.strategy.advise_indicators = \
|
self.strategy.advise_indicators = \
|
||||||
self.custom_hyperopt.populate_indicators # type: ignore
|
self.custom_hyperopt.populate_indicators # type: ignore
|
||||||
dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
||||||
|
@ -293,7 +307,7 @@ def start(args: Namespace) -> None:
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
# Monkey patch the configuration with hyperopt_conf.py
|
# Monkey patch the configuration with hyperopt_conf.py
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args, RunMode.HYPEROPT)
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
config = configuration.load_config()
|
config = configuration.load_config()
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,13 @@ class IHyperOpt(ABC):
|
||||||
Create a buy strategy generator
|
Create a buy strategy generator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Create a sell strategy generator
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def indicator_space() -> List[Dimension]:
|
def indicator_space() -> List[Dimension]:
|
||||||
|
@ -44,6 +51,13 @@ class IHyperOpt(ABC):
|
||||||
Create an indicator space
|
Create an indicator space
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create a sell indicator space
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
|
|
|
@ -83,7 +83,7 @@ def check_migrate(engine) -> None:
|
||||||
logger.debug(f'trying {table_back_name}')
|
logger.debug(f'trying {table_back_name}')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'stoploss_order_id'):
|
if not has_column(cols, 'stoploss_last_update'):
|
||||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||||
|
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
|
@ -93,6 +93,7 @@ def check_migrate(engine) -> None:
|
||||||
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||||
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||||
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
|
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
|
||||||
|
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
|
||||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
|
@ -111,7 +112,8 @@ def check_migrate(engine) -> None:
|
||||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id,
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy,
|
stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update,
|
||||||
|
max_rate, sell_reason, strategy,
|
||||||
ticker_interval
|
ticker_interval
|
||||||
)
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
|
@ -127,9 +129,9 @@ def check_migrate(engine) -> None:
|
||||||
{close_rate_requested} close_rate_requested, close_profit,
|
{close_rate_requested} close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id,
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||||
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate,
|
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||||
{sell_reason} sell_reason, {strategy} strategy,
|
{max_rate} max_rate, {sell_reason} sell_reason,
|
||||||
{ticker_interval} ticker_interval
|
{strategy} strategy, {ticker_interval} ticker_interval
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
@ -185,6 +187,8 @@ class Trade(_DECL_BASE):
|
||||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
# stoploss order id which is on exchange
|
# stoploss order id which is on exchange
|
||||||
stoploss_order_id = Column(String, nullable=True, index=True)
|
stoploss_order_id = Column(String, nullable=True, index=True)
|
||||||
|
# last update time of the stoploss order on exchange
|
||||||
|
stoploss_last_update = Column(DateTime, nullable=True)
|
||||||
# absolute value of the highest reached price
|
# absolute value of the highest reached price
|
||||||
max_rate = Column(Float, nullable=True, default=0.0)
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
sell_reason = Column(String, nullable=True)
|
sell_reason = Column(String, nullable=True)
|
||||||
|
@ -218,11 +222,13 @@ class Trade(_DECL_BASE):
|
||||||
logger.debug("assigning new stop loss")
|
logger.debug("assigning new stop loss")
|
||||||
self.stop_loss = new_loss
|
self.stop_loss = new_loss
|
||||||
self.initial_stop_loss = new_loss
|
self.initial_stop_loss = new_loss
|
||||||
|
self.stoploss_last_update = datetime.utcnow()
|
||||||
|
|
||||||
# evaluate if the stop loss needs to be updated
|
# evaluate if the stop loss needs to be updated
|
||||||
else:
|
else:
|
||||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||||
self.stop_loss = new_loss
|
self.stop_loss = new_loss
|
||||||
|
self.stoploss_last_update = datetime.utcnow()
|
||||||
logger.debug("adjusted stop loss")
|
logger.debug("adjusted stop loss")
|
||||||
else:
|
else:
|
||||||
logger.debug("keeping current stop loss")
|
logger.debug("keeping current stop loss")
|
||||||
|
|
|
@ -32,6 +32,13 @@ class HyperOptResolver(IResolver):
|
||||||
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
||||||
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
|
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||||
|
logger.warning("Custom Hyperopt does not provide populate_buy_trend. "
|
||||||
|
"Using populate_buy_trend from DefaultStrategy.")
|
||||||
|
if not hasattr(self.hyperopt, 'populate_sell_trend'):
|
||||||
|
logger.warning("Custom Hyperopt does not provide populate_sell_trend. "
|
||||||
|
"Using populate_sell_trend from DefaultStrategy.")
|
||||||
|
|
||||||
def _load_hyperopt(
|
def _load_hyperopt(
|
||||||
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
|
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
"""
|
"""
|
||||||
This module load custom strategies
|
This module load custom strategies
|
||||||
"""
|
"""
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from inspect import getfullargspec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
@ -39,59 +39,67 @@ class StrategyResolver(IResolver):
|
||||||
config=config,
|
config=config,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
|
# make sure experimental dict is available
|
||||||
|
if 'experimental' not in config:
|
||||||
|
config['experimental'] = {}
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
if 'minimal_roi' in config:
|
# (Attribute name, default, experimental)
|
||||||
self.strategy.minimal_roi = config['minimal_roi']
|
attributes = [("minimal_roi", None, False),
|
||||||
logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
|
("ticker_interval", None, False),
|
||||||
config['minimal_roi'])
|
("stoploss", None, False),
|
||||||
|
("trailing_stop", None, False),
|
||||||
|
("trailing_stop_positive", None, False),
|
||||||
|
("trailing_stop_positive_offset", 0.0, False),
|
||||||
|
("process_only_new_candles", None, False),
|
||||||
|
("order_types", None, False),
|
||||||
|
("order_time_in_force", None, False),
|
||||||
|
("use_sell_signal", False, True),
|
||||||
|
("sell_profit_only", False, True),
|
||||||
|
("ignore_roi_if_buy_signal", False, True),
|
||||||
|
]
|
||||||
|
for attribute, default, experimental in attributes:
|
||||||
|
if experimental:
|
||||||
|
self._override_attribute_helper(config['experimental'], attribute, default)
|
||||||
else:
|
else:
|
||||||
config['minimal_roi'] = self.strategy.minimal_roi
|
self._override_attribute_helper(config, attribute, default)
|
||||||
|
|
||||||
if 'stoploss' in config:
|
# Loop this list again to have output combined
|
||||||
self.strategy.stoploss = config['stoploss']
|
for attribute, _, exp in attributes:
|
||||||
logger.info(
|
if exp and attribute in config['experimental']:
|
||||||
"Override strategy 'stoploss' with value in config file: %s.", config['stoploss']
|
logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute])
|
||||||
)
|
elif attribute in config:
|
||||||
else:
|
logger.info("Strategy using %s: %s", attribute, config[attribute])
|
||||||
config['stoploss'] = self.strategy.stoploss
|
|
||||||
|
|
||||||
if 'ticker_interval' in config:
|
# Sort and apply type conversions
|
||||||
self.strategy.ticker_interval = config['ticker_interval']
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
logger.info(
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
||||||
"Override strategy 'ticker_interval' with value in config file: %s.",
|
key=lambda t: t[0]))
|
||||||
config['ticker_interval']
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
)
|
|
||||||
else:
|
|
||||||
config['ticker_interval'] = self.strategy.ticker_interval
|
|
||||||
|
|
||||||
if 'process_only_new_candles' in config:
|
self._strategy_sanity_validations()
|
||||||
self.strategy.process_only_new_candles = config['process_only_new_candles']
|
|
||||||
logger.info(
|
|
||||||
"Override process_only_new_candles 'process_only_new_candles' "
|
|
||||||
"with value in config file: %s.", config['process_only_new_candles']
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
config['process_only_new_candles'] = self.strategy.process_only_new_candles
|
|
||||||
|
|
||||||
if 'order_types' in config:
|
def _override_attribute_helper(self, config, attribute: str, default):
|
||||||
self.strategy.order_types = config['order_types']
|
"""
|
||||||
logger.info(
|
Override attributes in the strategy.
|
||||||
"Override strategy 'order_types' with value in config file: %s.",
|
Prevalence:
|
||||||
config['order_types']
|
- Configuration
|
||||||
)
|
- Strategy
|
||||||
else:
|
- default (if not None)
|
||||||
config['order_types'] = self.strategy.order_types
|
"""
|
||||||
|
if attribute in config:
|
||||||
if 'order_time_in_force' in config:
|
setattr(self.strategy, attribute, config[attribute])
|
||||||
self.strategy.order_time_in_force = config['order_time_in_force']
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||||
logger.info(
|
attribute, config[attribute])
|
||||||
"Override strategy 'order_time_in_force' with value in config file: %s.",
|
elif hasattr(self.strategy, attribute):
|
||||||
config['order_time_in_force']
|
config[attribute] = getattr(self.strategy, attribute)
|
||||||
)
|
# Explicitly check for None here as other "falsy" values are possible
|
||||||
else:
|
elif default is not None:
|
||||||
config['order_time_in_force'] = self.strategy.order_time_in_force
|
setattr(self.strategy, attribute, default)
|
||||||
|
config[attribute] = default
|
||||||
|
|
||||||
|
def _strategy_sanity_validations(self):
|
||||||
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
||||||
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
f"Order-types mapping is incomplete.")
|
f"Order-types mapping is incomplete.")
|
||||||
|
@ -100,12 +108,6 @@ class StrategyResolver(IResolver):
|
||||||
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
f"Order-time-in-force mapping is incomplete.")
|
f"Order-time-in-force mapping is incomplete.")
|
||||||
|
|
||||||
# Sort and apply type conversions
|
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
|
||||||
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
|
||||||
key=lambda t: t[0]))
|
|
||||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
|
||||||
|
|
||||||
def _load_strategy(
|
def _load_strategy(
|
||||||
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
|
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
|
@ -149,11 +151,9 @@ class StrategyResolver(IResolver):
|
||||||
if strategy:
|
if strategy:
|
||||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path)
|
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path)
|
||||||
strategy._populate_fun_len = len(
|
strategy._populate_fun_len = len(
|
||||||
inspect.getfullargspec(strategy.populate_indicators).args)
|
getfullargspec(strategy.populate_indicators).args)
|
||||||
strategy._buy_fun_len = len(
|
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
||||||
inspect.getfullargspec(strategy.populate_buy_trend).args)
|
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
|
||||||
strategy._sell_fun_len = len(
|
|
||||||
inspect.getfullargspec(strategy.populate_sell_trend).args)
|
|
||||||
|
|
||||||
return import_strategy(strategy, config=config)
|
return import_strategy(strategy, config=config)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|
|
@ -246,14 +246,14 @@ class Telegram(RPC):
|
||||||
stake_cur,
|
stake_cur,
|
||||||
fiat_disp_cur
|
fiat_disp_cur
|
||||||
)
|
)
|
||||||
stats = tabulate(stats,
|
stats_tab = tabulate(stats,
|
||||||
headers=[
|
headers=[
|
||||||
'Day',
|
'Day',
|
||||||
f'Profit {stake_cur}',
|
f'Profit {stake_cur}',
|
||||||
f'Profit {fiat_disp_cur}'
|
f'Profit {fiat_disp_cur}'
|
||||||
],
|
],
|
||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
|
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
|
@ -3,13 +3,26 @@
|
||||||
"""
|
"""
|
||||||
Bot state constant
|
Bot state constant
|
||||||
"""
|
"""
|
||||||
import enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
class State(Enum):
|
||||||
"""
|
"""
|
||||||
Bot application states
|
Bot application states
|
||||||
"""
|
"""
|
||||||
RUNNING = 0
|
RUNNING = 1
|
||||||
STOPPED = 1
|
STOPPED = 2
|
||||||
RELOAD_CONF = 2
|
RELOAD_CONF = 3
|
||||||
|
|
||||||
|
|
||||||
|
class RunMode(Enum):
|
||||||
|
"""
|
||||||
|
Bot running mode (backtest, hyperopt, ...)
|
||||||
|
can be "live", "dry-run", "backtest", "edgecli", "hyperopt".
|
||||||
|
"""
|
||||||
|
LIVE = "live"
|
||||||
|
DRY_RUN = "dry_run"
|
||||||
|
BACKTEST = "backtest"
|
||||||
|
EDGECLI = "edgecli"
|
||||||
|
HYPEROPT = "hyperopt"
|
||||||
|
OTHER = "other" # Used for plotting scripts and test
|
||||||
|
|
|
@ -42,6 +42,19 @@ class DefaultStrategy(IStrategy):
|
||||||
'sell': 'gtc',
|
'sell': 'gtc',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def informative_pairs(self):
|
||||||
|
"""
|
||||||
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
These pair/interval combinations are non-tradeable, unless they are part
|
||||||
|
of the whitelist as well.
|
||||||
|
For more information, please consult the documentation
|
||||||
|
:return: List of tuples in the format (pair, interval)
|
||||||
|
Sample: return [("ETH/USDT", "5m"),
|
||||||
|
("BTC/USDT", "15m"),
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
|
@ -13,7 +13,9 @@ import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -67,6 +69,11 @@ class IStrategy(ABC):
|
||||||
# associated stoploss
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
|
||||||
|
# trailing stoploss
|
||||||
|
trailing_stop: bool = False
|
||||||
|
trailing_stop_positive: float
|
||||||
|
trailing_stop_positive_offset: float
|
||||||
|
|
||||||
# associated ticker interval
|
# associated ticker interval
|
||||||
ticker_interval: str
|
ticker_interval: str
|
||||||
|
|
||||||
|
@ -75,7 +82,8 @@ class IStrategy(ABC):
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'limit',
|
'stoploss': 'limit',
|
||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False,
|
||||||
|
'stoploss_on_exchange_interval': 60,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional time in force
|
# Optional time in force
|
||||||
|
@ -87,12 +95,16 @@ class IStrategy(ABC):
|
||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
process_only_new_candles: bool = False
|
process_only_new_candles: bool = False
|
||||||
|
|
||||||
# Dict to determine if analysis is necessary
|
# Class level variables (intentional) containing
|
||||||
_last_candle_seen_per_pair: Dict[str, datetime] = {}
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||||
|
# and wallets - access to the current balance.
|
||||||
|
dp: DataProvider
|
||||||
|
wallets: Wallets
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self._last_candle_seen_per_pair = {}
|
# Dict to determine if analysis is necessary
|
||||||
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
@ -121,6 +133,19 @@ class IStrategy(ABC):
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def informative_pairs(self) -> List[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
These pair/interval combinations are non-tradeable, unless they are part
|
||||||
|
of the whitelist as well.
|
||||||
|
For more information, please consult the documentation
|
||||||
|
:return: List of tuples in the format (pair, interval)
|
||||||
|
Sample: return [("ETH/USDT", "5m"),
|
||||||
|
("BTC/USDT", "15m"),
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def get_strategy_name(self) -> str:
|
def get_strategy_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns strategy class name
|
Returns strategy class name
|
||||||
|
@ -141,19 +166,19 @@ class IStrategy(ABC):
|
||||||
if (not self.process_only_new_candles or
|
if (not self.process_only_new_candles or
|
||||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
||||||
# Defs that only make change on new candle data.
|
# Defs that only make change on new candle data.
|
||||||
logging.debug("TA Analysis Launched")
|
logger.debug("TA Analysis Launched")
|
||||||
dataframe = self.advise_indicators(dataframe, metadata)
|
dataframe = self.advise_indicators(dataframe, metadata)
|
||||||
dataframe = self.advise_buy(dataframe, metadata)
|
dataframe = self.advise_buy(dataframe, metadata)
|
||||||
dataframe = self.advise_sell(dataframe, metadata)
|
dataframe = self.advise_sell(dataframe, metadata)
|
||||||
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||||
else:
|
else:
|
||||||
logging.debug("Skippinig TA Analysis for already analyzed candle")
|
logger.debug("Skipping TA Analysis for already analyzed candle")
|
||||||
dataframe['buy'] = 0
|
dataframe['buy'] = 0
|
||||||
dataframe['sell'] = 0
|
dataframe['sell'] = 0
|
||||||
|
|
||||||
# Other Defs in strategy that want to be called every loop here
|
# Other Defs in strategy that want to be called every loop here
|
||||||
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||||
logging.debug("Loop Analysis Launched")
|
logger.debug("Loop Analysis Launched")
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -228,9 +253,6 @@ class IStrategy(ABC):
|
||||||
current_rate = low or rate
|
current_rate = low or rate
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
|
||||||
if self.order_types.get('stoploss_on_exchange'):
|
|
||||||
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
|
||||||
else:
|
|
||||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||||
current_time=date, current_profit=current_profit,
|
current_time=date, current_profit=current_profit,
|
||||||
force_stoploss=force_stoploss)
|
force_stoploss=force_stoploss)
|
||||||
|
@ -271,14 +293,16 @@ class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trailing_stop = self.config.get('trailing_stop', False)
|
trailing_stop = self.config.get('trailing_stop', False)
|
||||||
|
|
||||||
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
|
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
|
||||||
else self.stoploss, initial=True)
|
else self.stoploss, initial=True)
|
||||||
|
|
||||||
# evaluate if the stoploss was hit
|
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||||
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
if ((self.stoploss is not None) and
|
||||||
|
(trade.stop_loss >= current_rate) and
|
||||||
|
(not self.order_types.get('stoploss_on_exchange'))):
|
||||||
selltype = SellType.STOP_LOSS
|
selltype = SellType.STOP_LOSS
|
||||||
if trailing_stop:
|
# If Trailing stop (and max-rate did move above open rate)
|
||||||
|
if trailing_stop and trade.open_rate != trade.max_rate:
|
||||||
selltype = SellType.TRAILING_STOP_LOSS
|
selltype = SellType.TRAILING_STOP_LOSS
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"HIT STOP: current price at {current_rate:.6f}, "
|
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||||
|
@ -295,8 +319,9 @@ class IStrategy(ABC):
|
||||||
|
|
||||||
# check if we have a special stop loss for positive condition
|
# check if we have a special stop loss for positive condition
|
||||||
# and if profit is positive
|
# and if profit is positive
|
||||||
stop_loss_value = self.stoploss
|
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
||||||
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
|
|
||||||
|
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0
|
||||||
|
|
||||||
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
||||||
|
|
||||||
|
@ -313,15 +338,16 @@ class IStrategy(ABC):
|
||||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||||
sell
|
sell. Requires current_profit to be in percent!!
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
for duration, threshold in self.minimal_roi.items():
|
|
||||||
if time_diff <= duration:
|
# Get highest entry in ROI dict where key >= trade-duration
|
||||||
return False
|
roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys())))
|
||||||
|
threshold = self.minimal_roi[roi_entry]
|
||||||
if current_profit > threshold:
|
if current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
@ -27,6 +28,12 @@ def log_has(line, logs):
|
||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
|
def log_has_re(line, logs):
|
||||||
|
return reduce(lambda a, b: a or b,
|
||||||
|
filter(lambda x: re.match(line, x[2]), logs),
|
||||||
|
False)
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
@ -542,7 +549,7 @@ def ticker_history_list():
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history(ticker_history_list):
|
def ticker_history(ticker_history_list):
|
||||||
return parse_ticker_dataframe(ticker_history_list)
|
return parse_ticker_dataframe(ticker_history_list, "5m", True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -724,7 +731,7 @@ def tickers():
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file), '1m', True)
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
# Create an fixture/function
|
# Create an fixture/function
|
||||||
|
|
|
@ -1,25 +1,99 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data
|
||||||
|
from freqtrade.data.history import load_pair_history
|
||||||
|
from freqtrade.optimize import validate_backtest_data, get_timeframe
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
|
||||||
dataframe = parse_ticker_dataframe(result)
|
|
||||||
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
assert result.columns.tolist() == \
|
assert result.columns.tolist() == ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
['date', 'open', 'high', 'low', 'close', 'volume']
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ticker_dataframe(ticker_history, caplog):
|
def test_parse_ticker_dataframe(ticker_history_list, caplog):
|
||||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# Test file with BV data
|
# Test file with BV data
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
dataframe = parse_ticker_dataframe(ticker_history_list, '5m', fill_missing=True)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples)
|
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohlcv_fill_up_missing_data(caplog):
|
||||||
|
data = load_pair_history(datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=False,
|
||||||
|
pair='UNITTEST/BTC',
|
||||||
|
fill_up_missing=False)
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
data2 = ohlcv_fill_up_missing_data(data, '1m')
|
||||||
|
assert len(data2) > len(data)
|
||||||
|
# Column names should not change
|
||||||
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
|
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# Test fillup actually fixes invalid backtest data
|
||||||
|
min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
|
||||||
|
assert validate_backtest_data({'UNITTEST/BTC': data}, min_date, max_date, 1)
|
||||||
|
assert not validate_backtest_data({'UNITTEST/BTC': data2}, min_date, max_date, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohlcv_fill_up_missing_data2(caplog):
|
||||||
|
ticker_interval = '5m'
|
||||||
|
ticks = [[
|
||||||
|
1511686200000, # 8:50:00
|
||||||
|
8.794e-05, # open
|
||||||
|
8.948e-05, # high
|
||||||
|
8.794e-05, # low
|
||||||
|
8.88e-05, # close
|
||||||
|
2255, # volume (in quote currency)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1511686500000, # 8:55:00
|
||||||
|
8.88e-05,
|
||||||
|
8.942e-05,
|
||||||
|
8.88e-05,
|
||||||
|
8.893e-05,
|
||||||
|
9911,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1511687100000, # 9:05:00
|
||||||
|
8.891e-05,
|
||||||
|
8.893e-05,
|
||||||
|
8.875e-05,
|
||||||
|
8.877e-05,
|
||||||
|
2251
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1511687400000, # 9:10:00
|
||||||
|
8.877e-05,
|
||||||
|
8.883e-05,
|
||||||
|
8.895e-05,
|
||||||
|
8.817e-05,
|
||||||
|
123551
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Generate test-data without filling missing
|
||||||
|
data = parse_ticker_dataframe(ticks, ticker_interval, fill_missing=False)
|
||||||
|
assert len(data) == 3
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
data2 = ohlcv_fill_up_missing_data(data, ticker_interval)
|
||||||
|
assert len(data2) == 4
|
||||||
|
# 3rd candle has been filled
|
||||||
|
row = data2.loc[2, :]
|
||||||
|
assert row['volume'] == 0
|
||||||
|
# close shoult match close of previous candle
|
||||||
|
assert row['close'] == data.loc[1, 'close']
|
||||||
|
assert row['open'] == row['close']
|
||||||
|
assert row['high'] == row['close']
|
||||||
|
assert row['low'] == row['close']
|
||||||
|
# Column names should not change
|
||||||
|
assert (data.columns == data2.columns).all()
|
||||||
|
|
||||||
|
assert log_has(f"Missing data fillup: before: {len(data)} - after: {len(data2)}",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
92
freqtrade/tests/data/test_dataprovider.py
Normal file
92
freqtrade/tests/data/test_dataprovider.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.state import RunMode
|
||||||
|
from freqtrade.tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohlcv(mocker, default_conf, ticker_history):
|
||||||
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
|
tick_interval = default_conf["ticker_interval"]
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
exchange._klines[("XRP/BTC", tick_interval)] = ticker_history
|
||||||
|
exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.DRY_RUN
|
||||||
|
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", tick_interval))
|
||||||
|
assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame)
|
||||||
|
assert dp.ohlcv("UNITTEST/BTC", tick_interval) is not ticker_history
|
||||||
|
assert dp.ohlcv("UNITTEST/BTC", tick_interval, copy=False) is ticker_history
|
||||||
|
assert not dp.ohlcv("UNITTEST/BTC", tick_interval).empty
|
||||||
|
assert dp.ohlcv("NONESENSE/AAA", tick_interval).empty
|
||||||
|
|
||||||
|
# Test with and without parameter
|
||||||
|
assert dp.ohlcv("UNITTEST/BTC", tick_interval).equals(dp.ohlcv("UNITTEST/BTC"))
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.LIVE
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.LIVE
|
||||||
|
assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame)
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
assert dp.runmode == RunMode.BACKTEST
|
||||||
|
assert dp.ohlcv("UNITTEST/BTC", tick_interval).empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_historic_ohlcv(mocker, default_conf, ticker_history):
|
||||||
|
|
||||||
|
historymock = MagicMock(return_value=ticker_history)
|
||||||
|
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
|
||||||
|
|
||||||
|
# exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
dp = DataProvider(default_conf, None)
|
||||||
|
data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
|
||||||
|
assert isinstance(data, DataFrame)
|
||||||
|
assert historymock.call_count == 1
|
||||||
|
assert historymock.call_args_list[0][1]["datadir"] is None
|
||||||
|
assert historymock.call_args_list[0][1]["refresh_pairs"] is False
|
||||||
|
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
|
||||||
|
|
||||||
|
|
||||||
|
def test_available_pairs(mocker, default_conf, ticker_history):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
tick_interval = default_conf["ticker_interval"]
|
||||||
|
exchange._klines[("XRP/BTC", tick_interval)] = ticker_history
|
||||||
|
exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
|
||||||
|
assert len(dp.available_pairs) == 2
|
||||||
|
assert dp.available_pairs == [
|
||||||
|
("XRP/BTC", tick_interval),
|
||||||
|
("UNITTEST/BTC", tick_interval),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh(mocker, default_conf, ticker_history):
|
||||||
|
refresh_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
tick_interval = default_conf["ticker_interval"]
|
||||||
|
pairs = [("XRP/BTC", tick_interval), ("UNITTEST/BTC", tick_interval)]
|
||||||
|
|
||||||
|
pairs_non_trad = [("ETH/USDT", tick_interval), ("BTC/TUSD", "1h")]
|
||||||
|
|
||||||
|
dp = DataProvider(default_conf, exchange)
|
||||||
|
dp.refresh(pairs)
|
||||||
|
|
||||||
|
assert refresh_mock.call_count == 1
|
||||||
|
assert len(refresh_mock.call_args[0]) == 1
|
||||||
|
assert len(refresh_mock.call_args[0][0]) == len(pairs)
|
||||||
|
assert refresh_mock.call_args[0][0] == pairs
|
||||||
|
|
||||||
|
refresh_mock.reset_mock()
|
||||||
|
dp.refresh(pairs, pairs_non_trad)
|
||||||
|
assert refresh_mock.call_count == 1
|
||||||
|
assert len(refresh_mock.call_args[0]) == 1
|
||||||
|
assert len(refresh_mock.call_args[0][0]) == len(pairs) + len(pairs_non_trad)
|
||||||
|
assert refresh_mock.call_args[0][0] == pairs + pairs_non_trad
|
|
@ -450,7 +450,7 @@ def test_trim_tickerlist() -> None:
|
||||||
assert not ticker
|
assert not ticker
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json() -> None:
|
def test_file_dump_json_tofile() -> None:
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||||
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||||
data = {'bar': 'foo'}
|
data = {'bar': 'foo'}
|
||||||
|
|
|
@ -281,8 +281,8 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||||
123.45
|
123.45
|
||||||
] for x in range(0, 500)]
|
] for x in range(0, 500)]
|
||||||
|
|
||||||
pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC),
|
pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC, '1h', fill_missing=True),
|
||||||
'LTC/BTC': parse_ticker_dataframe(LTCBTC)}
|
'LTC/BTC': parse_ticker_dataframe(LTCBTC, '1h', fill_missing=True)}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -765,7 +765,7 @@ def test_get_history(default_conf, mocker, caplog):
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
|
|
||||||
async def mock_candle_hist(pair, tick_interval, since_ms):
|
async def mock_candle_hist(pair, tick_interval, since_ms):
|
||||||
return pair, tick
|
return pair, tick_interval, tick
|
||||||
|
|
||||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
|
@ -778,7 +778,7 @@ def test_get_history(default_conf, mocker, caplog):
|
||||||
assert len(ret) == 2
|
assert len(ret) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||||
tick = [
|
tick = [
|
||||||
[
|
[
|
||||||
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
|
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
|
||||||
|
@ -802,12 +802,12 @@ def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
|
|
||||||
pairs = ['IOTA/ETH', 'XRP/ETH']
|
pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
|
||||||
# empty dicts
|
# empty dicts
|
||||||
assert not exchange._klines
|
assert not exchange._klines
|
||||||
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
exchange.refresh_latest_ohlcv(pairs)
|
||||||
|
|
||||||
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
|
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog.record_tuples)
|
||||||
assert exchange._klines
|
assert exchange._klines
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
|
@ -822,10 +822,11 @@ def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||||
assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False)
|
assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False)
|
||||||
|
|
||||||
# test caching
|
# test caching
|
||||||
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
|
||||||
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
assert log_has(f"Using cached klines data for {pairs[0]} ...", caplog.record_tuples)
|
assert log_has(f"Using cached ohlcv data for {pairs[0][0]}, {pairs[0][1]} ...",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -850,11 +851,12 @@ async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
res = await exchange._async_get_candle_history(pair, "5m")
|
res = await exchange._async_get_candle_history(pair, "5m")
|
||||||
assert type(res) is tuple
|
assert type(res) is tuple
|
||||||
assert len(res) == 2
|
assert len(res) == 3
|
||||||
assert res[0] == pair
|
assert res[0] == pair
|
||||||
assert res[1] == tick
|
assert res[1] == "5m"
|
||||||
|
assert res[2] == tick
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog.record_tuples)
|
||||||
|
|
||||||
# exchange = Exchange(default_conf)
|
# exchange = Exchange(default_conf)
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
|
@ -883,44 +885,38 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
res = await exchange._async_get_candle_history(pair, "5m")
|
res = await exchange._async_get_candle_history(pair, "5m")
|
||||||
assert type(res) is tuple
|
assert type(res) is tuple
|
||||||
assert len(res) == 2
|
assert len(res) == 3
|
||||||
assert res[0] == pair
|
assert res[0] == pair
|
||||||
assert res[1] == tick
|
assert res[1] == "5m"
|
||||||
|
assert res[2] == tick
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
||||||
async def test_async_get_candles_history(default_conf, mocker):
|
|
||||||
tick = [
|
|
||||||
[
|
|
||||||
1511686200000, # unix timestamp ms
|
|
||||||
1, # open
|
|
||||||
2, # high
|
|
||||||
3, # low
|
|
||||||
4, # close
|
|
||||||
5, # volume (in quote currency)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
async def mock_get_candle_hist(pair, tick_interval, since_ms=None):
|
async def mock_get_candle_hist(pair, *args, **kwargs):
|
||||||
return (pair, tick)
|
if pair == 'ETH/BTC':
|
||||||
|
return [[]]
|
||||||
|
else:
|
||||||
|
raise TypeError()
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
# Monkey-patch async function
|
|
||||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
|
||||||
|
|
||||||
exchange._async_get_candle_history = Mock(wraps=mock_get_candle_hist)
|
# Monkey-patch async function with empty result
|
||||||
|
exchange._api_async.fetch_ohlcv = MagicMock(side_effect=mock_get_candle_hist)
|
||||||
|
|
||||||
|
pairs = [("ETH/BTC", "5m"), ("XRP/BTC", "5m")]
|
||||||
|
res = exchange.refresh_latest_ohlcv(pairs)
|
||||||
|
assert exchange._klines
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
|
|
||||||
pairs = ['ETH/BTC', 'XRP/BTC']
|
|
||||||
res = await exchange.async_get_candles_history(pairs, "5m")
|
|
||||||
assert type(res) is list
|
assert type(res) is list
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
assert type(res[0]) is tuple
|
# Test that each is in list at least once as order is not guaranteed
|
||||||
assert res[0][0] == pairs[0]
|
assert type(res[0]) is tuple or type(res[1]) is tuple
|
||||||
assert res[0][1] == tick
|
assert type(res[0]) is TypeError or type(res[1]) is TypeError
|
||||||
assert res[1][0] == pairs[1]
|
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples)
|
||||||
assert res[1][1] == tick
|
assert log_has("Async code raised an exception: TypeError", caplog.record_tuples)
|
||||||
assert exchange._async_get_candle_history.call_count == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_order_book(default_conf, mocker, order_book_l2):
|
def test_get_order_book(default_conf, mocker, order_book_l2):
|
||||||
|
@ -986,7 +982,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert res[0] == 'ETH/BTC'
|
assert res[0] == 'ETH/BTC'
|
||||||
ticks = res[1]
|
ticks = res[2]
|
||||||
|
|
||||||
assert sort_mock.call_count == 1
|
assert sort_mock.call_count == 1
|
||||||
assert ticks[0][0] == 1527830400000
|
assert ticks[0][0] == 1527830400000
|
||||||
|
@ -1023,7 +1019,8 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert res[0] == 'ETH/BTC'
|
assert res[0] == 'ETH/BTC'
|
||||||
ticks = res[1]
|
assert res[1] == default_conf['ticker_interval']
|
||||||
|
ticks = res[2]
|
||||||
# Sorted not called again - data is already in order
|
# Sorted not called again - data is already in order
|
||||||
assert sort_mock.call_count == 0
|
assert sort_mock.call_count == 0
|
||||||
assert ticks[0][0] == 1527827700000
|
assert ticks[0][0] == 1527827700000
|
||||||
|
|
|
@ -18,6 +18,7 @@ from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.optimize import get_timeframe
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
||||||
start)
|
start)
|
||||||
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
@ -75,7 +76,7 @@ def load_data_test(what):
|
||||||
pair[x][5] # Keep old volume
|
pair[x][5] # Keep old volume
|
||||||
] for x in range(0, datalen)
|
] for x in range(0, datalen)
|
||||||
]
|
]
|
||||||
return {'UNITTEST/BTC': parse_ticker_dataframe(data)}
|
return {'UNITTEST/BTC': parse_ticker_dataframe(data, '1m', fill_missing=True)}
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
|
@ -104,7 +105,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||||
timerange=None, exchange=None):
|
timerange=None, exchange=None):
|
||||||
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata)}
|
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', fill_missing=True)}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
@ -200,12 +201,15 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
|
|
||||||
assert 'timerange' not in config
|
assert 'timerange' not in config
|
||||||
assert 'export' not in config
|
assert 'export' not in config
|
||||||
|
assert 'runmode' in config
|
||||||
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
|
@ -229,6 +233,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using data folder: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
|
@ -322,15 +328,15 @@ def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
assert backtesting.fee == 0.5
|
assert backtesting.fee == 0.5
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99
|
assert len(data['UNITTEST/BTC']) == 102
|
||||||
|
|
||||||
# Load strategy to compare the result between Backtesting function and strategy are the same
|
# Load strategy to compare the result between Backtesting function and strategy are the same
|
||||||
strategy = DefaultStrategy(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
@ -340,6 +346,7 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_generate_text_table(default_conf, mocker):
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
default_conf['max_open_trades'] = 2
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
|
@ -355,13 +362,13 @@ def test_generate_text_table(default_conf, mocker):
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| pair | buy count | avg profit % | cum profit % | '
|
'| pair | buy count | avg profit % | cum profit % | '
|
||||||
'total profit BTC | avg duration | profit | loss |\n'
|
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||||
'|:--------|------------:|---------------:|---------------:|'
|
'|:--------|------------:|---------------:|---------------:|'
|
||||||
'-------------------:|:---------------|---------:|-------:|\n'
|
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||||
'0.60000000 | 0:20:00 | 2 | 0 |\n'
|
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||||
'0.60000000 | 0:20:00 | 2 | 0 |'
|
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
||||||
)
|
)
|
||||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
@ -397,6 +404,7 @@ def test_generate_text_table_strategyn(default_conf, mocker):
|
||||||
Test Backtesting.generate_text_table_sell_reason() method
|
Test Backtesting.generate_text_table_sell_reason() method
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
default_conf['max_open_trades'] = 2
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
results = {}
|
results = {}
|
||||||
results['ETH/BTC'] = pd.DataFrame(
|
results['ETH/BTC'] = pd.DataFrame(
|
||||||
|
@ -424,13 +432,13 @@ def test_generate_text_table_strategyn(default_conf, mocker):
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| Strategy | buy count | avg profit % | cum profit % '
|
'| Strategy | buy count | avg profit % | cum profit % '
|
||||||
'| total profit BTC | avg duration | profit | loss |\n'
|
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
||||||
'|:-----------|------------:|---------------:|---------------:'
|
'|:-----------|------------:|---------------:|---------------:'
|
||||||
'|-------------------:|:---------------|---------:|-------:|\n'
|
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
||||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||||
'| 1.10000000 | 0:17:00 | 3 | 0 |\n'
|
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
||||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||||
'| 1.30000000 | 0:20:00 | 3 | 0 |'
|
'| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |'
|
||||||
)
|
)
|
||||||
print(backtesting._generate_text_table_strategy(all_results=results))
|
print(backtesting._generate_text_table_strategy(all_results=results))
|
||||||
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
||||||
|
@ -442,7 +450,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||||
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
@ -477,7 +485,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
@ -526,13 +534,14 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
{'pair': [pair, pair],
|
{'pair': [pair, pair],
|
||||||
'profit_percent': [0.0, 0.0],
|
'profit_percent': [0.0, 0.0],
|
||||||
'profit_abs': [0.0, 0.0],
|
'profit_abs': [0.0, 0.0],
|
||||||
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
'open_time': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||||
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True
|
||||||
'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
),
|
||||||
Arrow(2018, 1, 30, 4, 15, 0).datetime],
|
'close_time': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
||||||
|
Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True),
|
||||||
'open_index': [78, 184],
|
'open_index': [78, 184],
|
||||||
'close_index': [125, 193],
|
'close_index': [125, 192],
|
||||||
'trade_duration': [235, 45],
|
'trade_duration': [235, 40],
|
||||||
'open_at_end': [False, False],
|
'open_at_end': [False, False],
|
||||||
'open_rate': [0.104445, 0.10302485],
|
'open_rate': [0.104445, 0.10302485],
|
||||||
'close_rate': [0.104969, 0.103541],
|
'close_rate': [0.104969, 0.103541],
|
||||||
|
@ -593,7 +602,7 @@ def test_processed(default_conf, mocker) -> None:
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
tests = [['raise', 18], ['lower', 0], ['sine', 19]]
|
tests = [['raise', 19], ['lower', 0], ['sine', 18]]
|
||||||
# We need to enable sell-signal - otherwise it sells on ROI!!
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
default_conf['experimental'] = {"use_sell_signal": True}
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from typing import List
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start)
|
from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start)
|
||||||
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +27,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args))
|
||||||
|
assert config['runmode'] == RunMode.EDGECLI
|
||||||
|
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
assert 'stake_amount' in config
|
assert 'stake_amount' in config
|
||||||
|
@ -46,10 +49,11 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
assert 'stoploss_range' not in config
|
assert 'stoploss_range' not in config
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(edge_conf)
|
read_data=json.dumps(edge_conf)
|
||||||
))
|
))
|
||||||
|
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
|
@ -69,6 +73,7 @@ def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||||
assert 'exchange' in config
|
assert 'exchange' in config
|
||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
|
assert config['runmode'] == RunMode.EDGECLI
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using data folder: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
|
|
|
@ -9,7 +9,8 @@ import pytest
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.data.history import load_tickerdata_file
|
from freqtrade.data.history import load_tickerdata_file
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, start
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
|
||||||
|
from freqtrade.resolvers import StrategyResolver, HyperOptResolver
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
|
@ -38,6 +39,28 @@ def create_trials(mocker, hyperopt) -> None:
|
||||||
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
hyperopts = DefaultHyperOpts
|
||||||
|
delattr(hyperopts, 'populate_buy_trend')
|
||||||
|
delattr(hyperopts, 'populate_sell_trend')
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
|
||||||
|
MagicMock(return_value=hyperopts)
|
||||||
|
)
|
||||||
|
x = HyperOptResolver(default_conf, ).hyperopt
|
||||||
|
assert not hasattr(x, 'populate_buy_trend')
|
||||||
|
assert not hasattr(x, 'populate_sell_trend')
|
||||||
|
assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
|
||||||
|
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples)
|
||||||
|
assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
|
||||||
|
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, default_conf, caplog) -> None:
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -201,7 +224,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
|
|
||||||
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
|
assert 'Best result:\nfoo result\nwith values:\n\n' in caplog.text
|
||||||
assert dumper.called
|
assert dumper.called
|
||||||
|
|
||||||
|
|
||||||
|
@ -243,7 +266,7 @@ def test_has_space(hyperopt):
|
||||||
|
|
||||||
def test_populate_indicators(hyperopt) -> None:
|
def test_populate_indicators(hyperopt) -> None:
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
{'pair': 'UNITTEST/BTC'})
|
{'pair': 'UNITTEST/BTC'})
|
||||||
|
@ -256,7 +279,7 @@ def test_populate_indicators(hyperopt) -> None:
|
||||||
|
|
||||||
def test_buy_strategy_generator(hyperopt) -> None:
|
def test_buy_strategy_generator(hyperopt) -> None:
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', fill_missing=True)}
|
||||||
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
{'pair': 'UNITTEST/BTC'})
|
{'pair': 'UNITTEST/BTC'})
|
||||||
|
@ -312,6 +335,15 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
'mfi-enabled': False,
|
'mfi-enabled': False,
|
||||||
'rsi-enabled': False,
|
'rsi-enabled': False,
|
||||||
'trigger': 'macd_cross_signal',
|
'trigger': 'macd_cross_signal',
|
||||||
|
'sell-adx-value': 0,
|
||||||
|
'sell-fastd-value': 75,
|
||||||
|
'sell-mfi-value': 0,
|
||||||
|
'sell-rsi-value': 0,
|
||||||
|
'sell-adx-enabled': False,
|
||||||
|
'sell-fastd-enabled': True,
|
||||||
|
'sell-mfi-enabled': False,
|
||||||
|
'sell-rsi-enabled': False,
|
||||||
|
'sell-trigger': 'macd_cross_signal',
|
||||||
'roi_t1': 60.0,
|
'roi_t1': 60.0,
|
||||||
'roi_t2': 30.0,
|
'roi_t2': 30.0,
|
||||||
'roi_t3': 20.0,
|
'roi_t3': 20.0,
|
||||||
|
|
|
@ -30,7 +30,8 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
|
||||||
history.load_data(
|
history.load_data(
|
||||||
datadir=None,
|
datadir=None,
|
||||||
ticker_interval='1m',
|
ticker_interval='1m',
|
||||||
pairs=['UNITTEST/BTC']
|
pairs=['UNITTEST/BTC'],
|
||||||
|
fill_up_missing=False
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
min_date, max_date = optimize.get_timeframe(data)
|
min_date, max_date = optimize.get_timeframe(data)
|
||||||
|
|
|
@ -117,7 +117,7 @@ def test_fiat_convert_get_price(mocker):
|
||||||
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||||
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||||
assert fiat_convert._pairs[0].price == 28000.0
|
assert fiat_convert._pairs[0].price == 28000.0
|
||||||
assert fiat_convert._pairs[0]._expiration is not 0
|
assert fiat_convert._pairs[0]._expiration != 0
|
||||||
assert len(fiat_convert._pairs) == 1
|
assert len(fiat_convert._pairs) == 1
|
||||||
|
|
||||||
# Verify the cached is used
|
# Verify the cached is used
|
||||||
|
|
|
@ -10,7 +10,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True)
|
||||||
|
|
||||||
|
|
||||||
def test_default_strategy_structure():
|
def test_default_strategy_structure():
|
||||||
|
|
|
@ -111,14 +111,19 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', True)}
|
||||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
assert len(data['UNITTEST/BTC']) == 102 # partial candle was removed
|
||||||
|
|
||||||
|
|
||||||
def test_min_roi_reached(default_conf, fee) -> None:
|
def test_min_roi_reached(default_conf, fee) -> None:
|
||||||
|
|
||||||
|
# Use list to confirm sequence does not matter
|
||||||
|
min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1},
|
||||||
|
{0: 0.1, 20: 0.05, 55: 0.01}]
|
||||||
|
for roi in min_roi_list:
|
||||||
strategy = DefaultStrategy(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
|
strategy.minimal_roi = roi
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
|
@ -129,8 +134,8 @@ def test_min_roi_reached(default_conf, fee) -> None:
|
||||||
open_rate=1,
|
open_rate=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime)
|
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
|
||||||
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
|
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
|
||||||
|
|
||||||
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
|
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
|
@ -139,6 +144,47 @@ def test_min_roi_reached(default_conf, fee) -> None:
|
||||||
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
|
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
|
||||||
|
|
||||||
|
|
||||||
|
def test_min_roi_reached2(default_conf, fee) -> None:
|
||||||
|
|
||||||
|
# test with ROI raising after last interval
|
||||||
|
min_roi_list = [{20: 0.07,
|
||||||
|
30: 0.05,
|
||||||
|
55: 0.30,
|
||||||
|
0: 0.1
|
||||||
|
},
|
||||||
|
{0: 0.1,
|
||||||
|
20: 0.07,
|
||||||
|
30: 0.05,
|
||||||
|
55: 0.30
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for roi in min_roi_list:
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
strategy.minimal_roi = roi
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
|
||||||
|
|
||||||
|
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
@ -158,7 +204,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
|
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
|
@ -168,7 +214,7 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||||
assert buy_mock.call_count == 2
|
assert buy_mock.call_count == 2
|
||||||
assert buy_mock.call_count == 2
|
assert buy_mock.call_count == 2
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
@ -196,7 +242,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
assert not log_has('Skipping TA Analysis for already analyzed candle',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
|
@ -211,5 +257,5 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
assert ret['buy'].sum() == 0
|
assert ret['buy'].sum() == 0
|
||||||
assert ret['sell'].sum() == 0
|
assert ret['sell'].sum() == 0
|
||||||
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
assert log_has('Skippinig TA Analysis for already analyzed candle',
|
assert log_has('Skipping TA Analysis for already analyzed candle',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
|
@ -150,6 +150,45 @@ def test_strategy_override_stoploss(caplog):
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_trailing_stop(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'trailing_stop': True
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.trailing_stop
|
||||||
|
assert isinstance(resolver.strategy.trailing_stop, bool)
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'trailing_stop' with value in config file: True."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_trailing_stop_positive(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'trailing_stop_positive': -0.1,
|
||||||
|
'trailing_stop_positive_offset': -0.2
|
||||||
|
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.trailing_stop_positive == -0.1
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
assert resolver.strategy.trailing_stop_positive_offset == -0.2
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
def test_strategy_override_ticker_interval(caplog):
|
def test_strategy_override_ticker_interval(caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
@ -178,8 +217,7 @@ def test_strategy_override_process_only_new_candles(caplog):
|
||||||
assert resolver.strategy.process_only_new_candles
|
assert resolver.strategy.process_only_new_candles
|
||||||
assert ('freqtrade.resolvers.strategy_resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override process_only_new_candles 'process_only_new_candles' "
|
"Override strategy 'process_only_new_candles' with value in config file: True."
|
||||||
"with value in config file: True."
|
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,6 +294,62 @@ def test_strategy_override_order_tif(caplog):
|
||||||
StrategyResolver(config)
|
StrategyResolver(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_use_sell_signal(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
assert not resolver.strategy.use_sell_signal
|
||||||
|
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||||
|
# must be inserted to configuration
|
||||||
|
assert 'use_sell_signal' in config['experimental']
|
||||||
|
assert not config['experimental']['use_sell_signal']
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'experimental': {
|
||||||
|
'use_sell_signal': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.use_sell_signal
|
||||||
|
assert isinstance(resolver.strategy.use_sell_signal, bool)
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'use_sell_signal' with value in config file: True."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_use_sell_profit_only(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
assert not resolver.strategy.sell_profit_only
|
||||||
|
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||||
|
# must be inserted to configuration
|
||||||
|
assert 'sell_profit_only' in config['experimental']
|
||||||
|
assert not config['experimental']['sell_profit_only']
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'experimental': {
|
||||||
|
'sell_profit_only': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.sell_profit_only
|
||||||
|
assert isinstance(resolver.strategy.sell_profit_only, bool)
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'sell_profit_only' with value in config file: True."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
def test_deprecate_populate_indicators(result):
|
def test_deprecate_populate_indicators(result):
|
||||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||||
|
|
|
@ -47,7 +47,7 @@ def test_scripts_options() -> None:
|
||||||
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
||||||
arguments.scripts_options()
|
arguments.scripts_options()
|
||||||
args = arguments.get_parsed_arg()
|
args = arguments.get_parsed_arg()
|
||||||
assert args.pair == 'ETH/BTC'
|
assert args.pairs == 'ETH/BTC'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_version() -> None:
|
def test_parse_args_version() -> None:
|
||||||
|
|
|
@ -13,6 +13,7 @@ from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration, set_loggers
|
from freqtrade.configuration import Configuration, set_loggers
|
||||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||||
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,11 +74,12 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
|
||||||
args = Arguments([], '').get_parsed_arg()
|
args = Arguments([], '').get_parsed_arg()
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
print(validated_conf)
|
|
||||||
|
|
||||||
assert validated_conf['max_open_trades'] > 999999999
|
assert validated_conf['max_open_trades'] > 999999999
|
||||||
assert validated_conf['max_open_trades'] == float('inf')
|
assert validated_conf['max_open_trades'] == float('inf')
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
assert "runmode" in validated_conf
|
||||||
|
assert validated_conf['runmode'] == RunMode.DRY_RUN
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_file_exception(mocker) -> None:
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
|
@ -125,6 +127,43 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
assert validated_conf.get('strategy_path') == '/some/path'
|
assert validated_conf.get('strategy_path') == '/some/path'
|
||||||
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||||
|
|
||||||
|
# Test conf provided db_url prod
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = False
|
||||||
|
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||||
|
|
||||||
|
# Test conf provided db_url dry_run
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = True
|
||||||
|
conf["db_url"] = "sqlite:///path/to/db.sqlite"
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == "sqlite:///path/to/db.sqlite"
|
||||||
|
|
||||||
|
# Test args provided db_url prod
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf["dry_run"] = False
|
conf["dry_run"] = False
|
||||||
del conf["db_url"]
|
del conf["db_url"]
|
||||||
|
@ -141,8 +180,10 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
||||||
|
assert "runmode" in validated_conf
|
||||||
|
assert validated_conf['runmode'] == RunMode.LIVE
|
||||||
|
|
||||||
# Test dry=run with ProdURL
|
# Test args provided db_url dry_run
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf["dry_run"] = True
|
conf["dry_run"] = True
|
||||||
conf["db_url"] = DEFAULT_DB_PROD_URL
|
conf["db_url"] = DEFAULT_DB_PROD_URL
|
||||||
|
@ -247,6 +288,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x)
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
|
@ -328,8 +370,9 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
|
||||||
|
|
||||||
args = Arguments(arglist, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args, RunMode.BACKTEST)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
assert config['runmode'] == RunMode.BACKTEST
|
||||||
assert 'max_open_trades' in config
|
assert 'max_open_trades' in config
|
||||||
assert 'stake_currency' in config
|
assert 'stake_currency' in config
|
||||||
assert 'stake_amount' in config
|
assert 'stake_amount' in config
|
||||||
|
@ -374,7 +417,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
]
|
]
|
||||||
args = Arguments(arglist, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args, RunMode.HYPEROPT)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
|
|
||||||
assert 'epochs' in config
|
assert 'epochs' in config
|
||||||
|
@ -385,6 +428,8 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert config['spaces'] == ['all']
|
assert config['spaces'] == ['all']
|
||||||
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
||||||
|
assert "runmode" in config
|
||||||
|
assert config['runmode'] == RunMode.HYPEROPT
|
||||||
|
|
||||||
|
|
||||||
def test_check_exchange(default_conf, caplog) -> None:
|
def test_check_exchange(default_conf, caplog) -> None:
|
||||||
|
@ -487,3 +532,13 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
def test_validate_default_conf(default_conf) -> None:
|
def test_validate_default_conf(default_conf) -> None:
|
||||||
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
|
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
|
||||||
|
|
||||||
|
|
||||||
|
def test__create_datadir(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch('os.path.isdir', MagicMock(return_value=False))
|
||||||
|
md = MagicMock()
|
||||||
|
mocker.patch('os.makedirs', md)
|
||||||
|
cfg = Configuration(Namespace())
|
||||||
|
cfg._create_datadir(default_conf, '/foo/bar')
|
||||||
|
assert md.call_args[0][0] == "/foo/bar"
|
||||||
|
assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
|
||||||
|
|
|
@ -18,7 +18,7 @@ from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType, SellCheckTuple
|
from freqtrade.strategy.interface import SellType, SellCheckTuple
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange, patch_edge, patch_wallet
|
from freqtrade.tests.conftest import log_has, log_has_re, patch_exchange, patch_edge, patch_wallet
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
freqtrade.strategy.get_signal = lambda e, s, t: value
|
freqtrade.strategy.get_signal = lambda e, s, t: value
|
||||||
freqtrade.exchange.refresh_tickers = lambda p, i: None
|
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
||||||
|
|
||||||
|
|
||||||
def patch_RPCManager(mocker) -> MagicMock:
|
def patch_RPCManager(mocker) -> MagicMock:
|
||||||
|
@ -691,7 +691,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||||
assert trade.amount == 90.99181073703367
|
assert trade.amount == 90.99181073703367
|
||||||
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Buy signal found: about create a new trade with stake_amount: 0.001000 ...',
|
'Buy signal found: about create a new trade with stake_amount: 0.001 ...',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -807,25 +807,61 @@ def test_process_trade_no_whitelist_pair(
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
def _refresh_whitelist(list):
|
||||||
|
return ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
|
||||||
|
|
||||||
|
refresh_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_markets=markets,
|
||||||
|
buy=MagicMock(side_effect=TemporaryError),
|
||||||
|
refresh_latest_ohlcv=refresh_mock,
|
||||||
|
)
|
||||||
|
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
|
||||||
|
mocker.patch('time.sleep', return_value=None)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.pairlists._validate_whitelist = _refresh_whitelist
|
||||||
|
freqtrade.strategy.informative_pairs = inf_pairs
|
||||||
|
# patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
freqtrade._process()
|
||||||
|
assert inf_pairs.call_count == 1
|
||||||
|
assert refresh_mock.call_count == 1
|
||||||
|
assert ("BTC/ETH", "1m") in refresh_mock.call_args[0][0]
|
||||||
|
assert ("ETH/USDT", "1h") in refresh_mock.call_args[0][0]
|
||||||
|
assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker, default_conf) -> None:
|
def test_balance_fully_ask_side(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={'ask': 20, 'last': 10}))
|
||||||
|
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 20
|
assert freqtrade.get_target_bid('ETH/BTC') == 20
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker, default_conf) -> None:
|
def test_balance_fully_last_side(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(return_value={'ask': 20, 'last': 10}))
|
||||||
|
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 10
|
assert freqtrade.get_target_bid('ETH/BTC') == 10
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
|
MagicMock(return_value={'ask': 5, 'last': 10}))
|
||||||
|
assert freqtrade.get_target_bid('ETH/BTC') == 5
|
||||||
|
|
||||||
|
|
||||||
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
|
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
|
||||||
|
@ -1014,6 +1050,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
# When trailing stoploss is set
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# enabling TSL
|
||||||
|
default_conf['trailing_stop'] = True
|
||||||
|
|
||||||
|
# disabling ROI
|
||||||
|
default_conf['minimal_roi']['0'] = 999999999
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
# enabling stoploss on exchange
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
# setting stoploss
|
||||||
|
freqtrade.strategy.stoploss = -0.05
|
||||||
|
|
||||||
|
# setting stoploss_on_exchange_interval to 60 seconds
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
||||||
|
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
|
stoploss_order_hanging = MagicMock(return_value={
|
||||||
|
'id': 100,
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'info': {
|
||||||
|
'stopPrice': '0.000011134'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||||
|
|
||||||
|
# stoploss initially at 5%
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
|
# price jumped 2x
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||||
|
'bid': 0.00002344,
|
||||||
|
'ask': 0.00002346,
|
||||||
|
'last': 0.00002344
|
||||||
|
}))
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
stoploss_order_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||||
|
|
||||||
|
# stoploss should not be updated as the interval is 60 seconds
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
cancel_order_mock.assert_not_called()
|
||||||
|
stoploss_order_mock.assert_not_called()
|
||||||
|
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert trade.stop_loss == 0.00002344 * 0.95
|
||||||
|
|
||||||
|
# setting stoploss_on_exchange_interval to 0 seconds
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
|
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||||
|
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
|
||||||
|
pair='ETH/BTC',
|
||||||
|
rate=0.00002344 * 0.95 * 0.99,
|
||||||
|
stop_price=0.00002344 * 0.95)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
|
||||||
|
# When trailing stoploss is set
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
patch_edge(mocker)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# enabling TSL
|
||||||
|
edge_conf['trailing_stop'] = True
|
||||||
|
edge_conf['trailing_stop_positive'] = 0.01
|
||||||
|
edge_conf['trailing_stop_positive_offset'] = 0.011
|
||||||
|
|
||||||
|
# disabling ROI
|
||||||
|
edge_conf['minimal_roi']['0'] = 999999999
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(edge_conf)
|
||||||
|
|
||||||
|
# enabling stoploss on exchange
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
# setting stoploss
|
||||||
|
freqtrade.strategy.stoploss = -0.02
|
||||||
|
|
||||||
|
# setting stoploss_on_exchange_interval to 0 second
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
||||||
|
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
|
||||||
|
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
|
stoploss_order_hanging = MagicMock(return_value={
|
||||||
|
'id': 100,
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2,
|
||||||
|
'info': {
|
||||||
|
'stopPrice': '0.000009384'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||||
|
|
||||||
|
# stoploss initially at 20% as edge dictated it.
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert trade.stop_loss == 0.000009384
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
stoploss_order_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
||||||
|
|
||||||
|
# price goes down 5%
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||||
|
'bid': 0.00001172 * 0.95,
|
||||||
|
'ask': 0.00001173 * 0.95,
|
||||||
|
'last': 0.00001172 * 0.95
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
|
# stoploss should remain the same
|
||||||
|
assert trade.stop_loss == 0.000009384
|
||||||
|
|
||||||
|
# stoploss on exchange should not be canceled
|
||||||
|
cancel_order_mock.assert_not_called()
|
||||||
|
|
||||||
|
# price jumped 2x
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||||
|
'bid': 0.00002344,
|
||||||
|
'ask': 0.00002346,
|
||||||
|
'last': 0.00002344
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
|
# stoploss should be set to 1% as trailing is on
|
||||||
|
assert trade.stop_loss == 0.00002344 * 0.99
|
||||||
|
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||||
|
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
|
||||||
|
pair='NEO/BTC',
|
||||||
|
rate=0.00002344 * 0.99 * 0.99,
|
||||||
|
stop_price=0.00002344 * 0.99)
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -1082,7 +1323,7 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
|
||||||
side_effect=OperationalException()
|
side_effect=OperationalException()
|
||||||
)
|
)
|
||||||
freqtrade.process_maybe_execute_sell(trade)
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
assert log_has('could not update trade amount: ', caplog.record_tuples)
|
assert log_has('Could not update trade amount: ', caplog.record_tuples)
|
||||||
|
|
||||||
# Test raise of DependencyException exception
|
# Test raise of DependencyException exception
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -1318,6 +1559,47 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
|
||||||
assert nb_trades == 0
|
assert nb_trades == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old,
|
||||||
|
fee, mocker, caplog) -> None:
|
||||||
|
""" Handle Buy order cancelled on exchange"""
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
patch_exchange(mocker)
|
||||||
|
limit_buy_order_old.update({"status": "canceled"})
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_order=MagicMock(return_value=limit_buy_order_old),
|
||||||
|
cancel_order=cancel_order_mock,
|
||||||
|
get_fee=fee
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
trade_buy = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
open_rate=0.00001099,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_order_id='123456789',
|
||||||
|
amount=90.99181073,
|
||||||
|
fee_open=0.0,
|
||||||
|
fee_close=0.0,
|
||||||
|
stake_amount=1,
|
||||||
|
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
||||||
|
is_open=True
|
||||||
|
)
|
||||||
|
|
||||||
|
Trade.session.add(trade_buy)
|
||||||
|
|
||||||
|
# check it does cancel buy orders over the time limit
|
||||||
|
freqtrade.check_handle_timedout()
|
||||||
|
assert cancel_order_mock.call_count == 0
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert nb_trades == 0
|
||||||
|
assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old,
|
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old,
|
||||||
fee, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
@ -1392,6 +1674,45 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
||||||
assert trade_sell.is_open is True
|
assert trade_sell.is_open is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
|
||||||
|
mocker, caplog) -> None:
|
||||||
|
""" Handle sell order cancelled on exchange"""
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
limit_sell_order_old.update({"status": "canceled"})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_order=MagicMock(return_value=limit_sell_order_old),
|
||||||
|
cancel_order=cancel_order_mock
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
trade_sell = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
open_rate=0.00001099,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_order_id='123456789',
|
||||||
|
amount=90.99181073,
|
||||||
|
fee_open=0.0,
|
||||||
|
fee_close=0.0,
|
||||||
|
stake_amount=1,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-5).datetime,
|
||||||
|
close_date=arrow.utcnow().shift(minutes=-601).datetime,
|
||||||
|
is_open=False
|
||||||
|
)
|
||||||
|
|
||||||
|
Trade.session.add(trade_sell)
|
||||||
|
|
||||||
|
# check it does cancel sell orders over the time limit
|
||||||
|
freqtrade.check_handle_timedout()
|
||||||
|
assert cancel_order_mock.call_count == 0
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
assert trade_sell.is_open is True
|
||||||
|
assert log_has_re("Sell order canceled on exchange for Trade.*", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
||||||
mocker) -> None:
|
mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
@ -1508,7 +1829,8 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
||||||
|
|
||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
'amount': 1}
|
'amount': 1,
|
||||||
|
'status': "open"}
|
||||||
assert freqtrade.handle_timedout_limit_sell(trade, order)
|
assert freqtrade.handle_timedout_limit_sell(trade, order)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
order['amount'] = 2
|
order['amount'] = 2
|
||||||
|
@ -2066,6 +2388,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
trade.max_rate = trade.open_rate * 1.003
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
# Sell as trailing-stop is reached
|
# Sell as trailing-stop is reached
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
@ -2495,10 +2818,13 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
|
||||||
instead of the ask rate
|
instead of the ask rate
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
get_order_book=order_book_l2
|
get_order_book=order_book_l2,
|
||||||
|
get_ticker=ticker_mock,
|
||||||
|
|
||||||
)
|
)
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
|
@ -2507,7 +2833,8 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
|
||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.045, 'last': 0.046}) == 0.043935
|
assert freqtrade.get_target_bid('ETH/BTC') == 0.043935
|
||||||
|
assert ticker_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
|
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
|
||||||
|
@ -2516,10 +2843,13 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
|
||||||
instead of the order book rate (even if enabled)
|
instead of the order book rate (even if enabled)
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
get_order_book=order_book_l2
|
get_order_book=order_book_l2,
|
||||||
|
get_ticker=ticker_mock,
|
||||||
|
|
||||||
)
|
)
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
|
@ -2528,29 +2858,9 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
|
||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.042, 'last': 0.046}) == 0.042
|
# ordrebook shall be used even if tickers would be lower.
|
||||||
|
assert freqtrade.get_target_bid('ETH/BTC', ) != 0.042
|
||||||
|
assert ticker_mock.call_count == 0
|
||||||
def test_order_book_bid_strategy3(default_conf, mocker, order_book_l2, markets) -> None:
|
|
||||||
"""
|
|
||||||
test if function get_target_bid will return ask rate instead
|
|
||||||
of the order book rate
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
get_markets=markets,
|
|
||||||
get_order_book=order_book_l2
|
|
||||||
)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
|
||||||
default_conf['bid_strategy']['order_book_top'] = 1
|
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0
|
|
||||||
default_conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
|
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.03, 'last': 0.029}) == 0.03
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:
|
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:
|
||||||
|
|
|
@ -5,8 +5,8 @@ from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
||||||
file_dump_json, format_ms_time, shorten_date)
|
file_dump_json, file_load_json, format_ms_time, shorten_date)
|
||||||
from freqtrade.data.history import load_tickerdata_file
|
from freqtrade.data.history import load_tickerdata_file, make_testdata_path
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ def test_shorten_date() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_datesarray_to_datetimearray(ticker_history_list):
|
def test_datesarray_to_datetimearray(ticker_history_list):
|
||||||
dataframes = parse_ticker_dataframe(ticker_history_list)
|
dataframes = parse_ticker_dataframe(ticker_history_list, "5m", fill_missing=True)
|
||||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
assert isinstance(dates[0], datetime.datetime)
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
|
@ -34,29 +34,42 @@ def test_datesarray_to_datetimearray(ticker_history_list):
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
strategy = DefaultStrategy(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", fill_missing=True)}
|
||||||
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
dates = common_datearray(dataframes)
|
dates = common_datearray(dataframes)
|
||||||
|
|
||||||
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
|
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
|
||||||
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
|
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
|
||||||
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1]
|
assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1]
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json(mocker) -> None:
|
def test_file_dump_json(mocker) -> None:
|
||||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||||
json_dump = mocker.patch('json.dump', MagicMock())
|
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||||
file_dump_json('somefile', [1, 2, 3])
|
file_dump_json('somefile', [1, 2, 3])
|
||||||
assert file_open.call_count == 1
|
assert file_open.call_count == 1
|
||||||
assert json_dump.call_count == 1
|
assert json_dump.call_count == 1
|
||||||
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
||||||
json_dump = mocker.patch('json.dump', MagicMock())
|
json_dump = mocker.patch('rapidjson.dump', MagicMock())
|
||||||
file_dump_json('somefile', [1, 2, 3], True)
|
file_dump_json('somefile', [1, 2, 3], True)
|
||||||
assert file_open.call_count == 1
|
assert file_open.call_count == 1
|
||||||
assert json_dump.call_count == 1
|
assert json_dump.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_load_json(mocker) -> None:
|
||||||
|
|
||||||
|
# 7m .json does not exist
|
||||||
|
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-7m.json'))
|
||||||
|
assert not ret
|
||||||
|
# 1m json exists (but no .gz exists)
|
||||||
|
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-1m.json'))
|
||||||
|
assert ret
|
||||||
|
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||||
|
ret = file_load_json(make_testdata_path(None).joinpath('UNITTEST_BTC-8m.json'))
|
||||||
|
assert ret
|
||||||
|
|
||||||
|
|
||||||
def test_format_ms_time() -> None:
|
def test_format_ms_time() -> None:
|
||||||
# Date 2018-04-10 18:02:01
|
# Date 2018-04-10 18:02:01
|
||||||
date_in_epoch_ms = 1523383321000
|
date_in_epoch_ms = 1523383321000
|
||||||
|
|
|
@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
assert trade.strategy is None
|
assert trade.strategy is None
|
||||||
assert trade.ticker_interval is None
|
assert trade.ticker_interval is None
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.stoploss_last_update is None
|
||||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
assert log_has("Running database migration - backup available as trades_bak2",
|
assert log_has("Running database migration - backup available as trades_bak2",
|
||||||
|
|
2
freqtrade/vendor/qtpylib/indicators.py
vendored
2
freqtrade/vendor/qtpylib/indicators.py
vendored
|
@ -236,7 +236,7 @@ def crossed(series1, series2, direction=None):
|
||||||
if direction is None:
|
if direction is None:
|
||||||
return above or below
|
return above or below
|
||||||
|
|
||||||
return above if direction is "above" else below
|
return above if direction == "above" else below
|
||||||
|
|
||||||
|
|
||||||
def crossed_above(series1, series2):
|
def crossed_above(series1, series2):
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
""" Wallet """
|
""" Wallet """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, NamedTuple
|
from typing import Dict, Any, NamedTuple
|
||||||
from collections import namedtuple
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# wallet data structure
|
||||||
class Wallet(NamedTuple):
|
class Wallet(NamedTuple):
|
||||||
exchange: str
|
exchange: str
|
||||||
currency: str
|
currency: str
|
||||||
|
@ -18,12 +18,6 @@ class Wallet(NamedTuple):
|
||||||
|
|
||||||
class Wallets(object):
|
class Wallets(object):
|
||||||
|
|
||||||
# wallet data structure
|
|
||||||
wallet = namedtuple(
|
|
||||||
'wallet',
|
|
||||||
['exchange', 'currency', 'free', 'used', 'total']
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, exchange: Exchange) -> None:
|
def __init__(self, exchange: Exchange) -> None:
|
||||||
self.exchange = exchange
|
self.exchange = exchange
|
||||||
self.wallets: Dict[str, Any] = {}
|
self.wallets: Dict[str, Any] = {}
|
||||||
|
|
43
mkdocs.yml
Normal file
43
mkdocs.yml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
site_name: Freqtrade
|
||||||
|
nav:
|
||||||
|
- About: index.md
|
||||||
|
- Installation: installation.md
|
||||||
|
- Configuration: configuration.md
|
||||||
|
- Start the bot: bot-usage.md
|
||||||
|
- Stoploss: stoploss.md
|
||||||
|
- Custom Strategy: bot-optimization.md
|
||||||
|
- Telegram: telegram-usage.md
|
||||||
|
- Web Hook: webhook-config.md
|
||||||
|
- Backtesting: backtesting.md
|
||||||
|
- Hyperopt: hyperopt.md
|
||||||
|
- Edge positioning: edge.md
|
||||||
|
- Plotting: plotting.md
|
||||||
|
- FAQ: faq.md
|
||||||
|
- SQL Cheatsheet: sql_cheatsheet.md
|
||||||
|
- Sanbox testing: sandbox-testing.md
|
||||||
|
- Contributors guide: developer.md
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
logo: 'images/logo.png'
|
||||||
|
custom_dir: 'docs'
|
||||||
|
palette:
|
||||||
|
primary: 'blue grey'
|
||||||
|
accent: 'tear'
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- codehilite:
|
||||||
|
guess_lang: false
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.arithmatex
|
||||||
|
- pymdownx.caret
|
||||||
|
- pymdownx.critic
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.magiclink
|
||||||
|
- pymdownx.mark
|
||||||
|
- pymdownx.smartsymbols
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- pymdownx.tilde
|
|
@ -1,8 +1,12 @@
|
||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
flake8==3.6.0
|
flake8==3.7.6
|
||||||
pytest==4.0.2
|
flake8-type-annotations==0.1.0
|
||||||
pytest-mock==1.10.0
|
flake8-tidy-imports==2.0.0
|
||||||
pytest-asyncio==0.9.0
|
pytest==4.3.0
|
||||||
pytest-cov==2.6.0
|
pytest-mock==1.10.1
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-cov==2.6.1
|
||||||
|
coveralls==1.6.0
|
||||||
|
mypy==0.670
|
||||||
|
|
5
requirements-plot.txt
Normal file
5
requirements-plot.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Include all requirements to run the bot.
|
||||||
|
-r requirements.txt
|
||||||
|
|
||||||
|
plotly==3.6.1
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
ccxt==1.18.71
|
ccxt==1.18.270
|
||||||
SQLAlchemy==1.2.15
|
SQLAlchemy==1.2.18
|
||||||
python-telegram-bot==11.1.0
|
python-telegram-bot==11.1.0
|
||||||
arrow==0.12.1
|
arrow==0.13.1
|
||||||
cachetools==3.0.0
|
cachetools==3.1.0
|
||||||
requests==2.21.0
|
requests==2.21.0
|
||||||
urllib3==1.24.1
|
urllib3==1.24.1
|
||||||
wrapt==1.10.11
|
wrapt==1.11.1
|
||||||
pandas==0.23.4
|
numpy==1.16.1
|
||||||
|
pandas==0.24.1
|
||||||
scikit-learn==0.20.2
|
scikit-learn==0.20.2
|
||||||
joblib==0.13.0
|
joblib==0.13.2
|
||||||
scipy==1.2.0
|
scipy==1.2.1
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.15.4
|
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
tabulate==0.8.2
|
tabulate==0.8.3
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
|
@ -23,4 +23,4 @@ scikit-optimize==0.5.2
|
||||||
py_find_1st==1.1.3
|
py_find_1st==1.1.3
|
||||||
|
|
||||||
#Load ticker files 30% faster
|
#Load ticker files 30% faster
|
||||||
ujson==1.35
|
python-rapidjson==0.7.0
|
||||||
|
|
|
@ -31,6 +31,7 @@ if args.config:
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration._load_config_file(args.config)
|
config = configuration._load_config_file(args.config)
|
||||||
|
|
||||||
|
config['stake_currency'] = ''
|
||||||
# Ensure we do not use Exchange credentials
|
# Ensure we do not use Exchange credentials
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Script to display when the bot will buy a specific pair
|
Script to display when the bot will buy on specific pair(s)
|
||||||
|
|
||||||
Mandatory Cli parameters:
|
Mandatory Cli parameters:
|
||||||
-p / --pair: pair to examine
|
-p / --pairs: pair(s) to examine
|
||||||
|
|
||||||
Option but recommended
|
Option but recommended
|
||||||
-s / --strategy: strategy to use
|
-s / --strategy: strategy to use
|
||||||
|
|
||||||
|
|
||||||
Optional Cli parameters
|
Optional Cli parameters
|
||||||
-d / --datadir: path to pair backtest data
|
-d / --datadir: path to pair(s) backtest data
|
||||||
--timerange: specify what timerange of data to use.
|
--timerange: specify what timerange of data to use.
|
||||||
-l / --live: Live, to download the latest ticker for the pair
|
-l / --live: Live, to download the latest ticker for the pair(s)
|
||||||
-db / --db-url: Show trades stored in database
|
-db / --db-url: Show trades stored in database
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ Row 1: sma, ema3, ema5, ema10, ema50
|
||||||
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
|
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
|
||||||
|
|
||||||
Example of usage:
|
Example of usage:
|
||||||
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
> python3 scripts/plot_dataframe.py --pairs BTC/EUR,XRP/BTC -d user_data/data/
|
||||||
--indicators2 fastk,fastd
|
--indicators1 sma,ema3 --indicators2 fastk,fastd
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
@ -65,7 +65,8 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
|
||||||
t.open_date.replace(tzinfo=timeZone),
|
t.open_date.replace(tzinfo=timeZone),
|
||||||
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
|
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
|
||||||
t.open_rate, t.close_rate,
|
t.open_rate, t.close_rate,
|
||||||
t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None)
|
t.close_date.timestamp() - t.open_date.timestamp()
|
||||||
|
if t.close_date else None)
|
||||||
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
|
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
|
||||||
columns=columns)
|
columns=columns)
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
|
||||||
# must align with columns in backtest.py
|
# must align with columns in backtest.py
|
||||||
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
if file.exists():
|
||||||
with file.open() as f:
|
with file.open() as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
trades = pd.DataFrame(data, columns=columns)
|
trades = pd.DataFrame(data, columns=columns)
|
||||||
|
@ -84,42 +86,55 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
|
||||||
if timerange.stoptype == 'date':
|
if timerange.stoptype == 'date':
|
||||||
trades = trades.loc[trades["opents"] <= timerange.stopts]
|
trades = trades.loc[trades["opents"] <= timerange.stopts]
|
||||||
|
|
||||||
trades['opents'] = pd.to_datetime(trades['opents'],
|
trades['opents'] = pd.to_datetime(
|
||||||
|
trades['opents'],
|
||||||
unit='s',
|
unit='s',
|
||||||
utc=True,
|
utc=True,
|
||||||
infer_datetime_format=True)
|
infer_datetime_format=True)
|
||||||
trades['closets'] = pd.to_datetime(trades['closets'],
|
trades['closets'] = pd.to_datetime(
|
||||||
|
trades['closets'],
|
||||||
unit='s',
|
unit='s',
|
||||||
utc=True,
|
utc=True,
|
||||||
infer_datetime_format=True)
|
infer_datetime_format=True)
|
||||||
|
else:
|
||||||
|
trades = pd.DataFrame([], columns=columns)
|
||||||
|
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args: Namespace) -> None:
|
def generate_plot_file(fig, pair, tick_interval, is_last) -> None:
|
||||||
"""
|
"""
|
||||||
Calls analyze() and plots the returned dataframe
|
Generate a plot html file from pre populated fig plotly object
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
logger.info('Generate plot file for %s', pair)
|
||||||
|
|
||||||
|
pair_name = pair.replace("/", "_")
|
||||||
|
file_name = 'freqtrade-plot-' + pair_name + '-' + tick_interval + '.html'
|
||||||
|
|
||||||
|
Path("user_data/plots").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)), auto_open=False)
|
||||||
|
if is_last:
|
||||||
|
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')), auto_open=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_env(args: Namespace):
|
||||||
|
"""
|
||||||
|
Initalize freqtrade Exchange and Strategy, split pairs recieved in parameter
|
||||||
|
:return: Strategy
|
||||||
|
"""
|
||||||
global _CONF
|
global _CONF
|
||||||
|
|
||||||
# Load the configuration
|
# Load the configuration
|
||||||
_CONF.update(setup_configuration(args))
|
_CONF.update(setup_configuration(args))
|
||||||
|
|
||||||
print(_CONF)
|
print(_CONF)
|
||||||
# Set the pair to audit
|
|
||||||
pair = args.pair
|
|
||||||
|
|
||||||
if pair is None:
|
pairs = args.pairs.split(',')
|
||||||
logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC')
|
if pairs is None:
|
||||||
|
logger.critical('Parameter --pairs mandatory;. E.g --pairs ETH/BTC,XRP/BTC')
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
if '/' not in pair:
|
|
||||||
logger.critical('--pair format must be XXX/YYY')
|
|
||||||
exit()
|
|
||||||
|
|
||||||
# Set timerange to use
|
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
|
||||||
|
|
||||||
# Load the strategy
|
# Load the strategy
|
||||||
try:
|
try:
|
||||||
strategy = StrategyResolver(_CONF).strategy
|
strategy = StrategyResolver(_CONF).strategy
|
||||||
|
@ -131,61 +146,84 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
)
|
)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
# Set the ticker to use
|
return [strategy, exchange, pairs]
|
||||||
tick_interval = strategy.ticker_interval
|
|
||||||
|
|
||||||
|
def get_tickers_data(strategy, exchange, pairs: List[str], args):
|
||||||
|
"""
|
||||||
|
Get tickers data for each pairs on live or local, option defined in args
|
||||||
|
:return: dictinnary of tickers. output format: {'pair': tickersdata}
|
||||||
|
"""
|
||||||
|
|
||||||
|
tick_interval = strategy.ticker_interval
|
||||||
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
# Load pair tickers
|
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pairs.')
|
||||||
exchange.refresh_tickers([pair], tick_interval)
|
exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs])
|
||||||
tickers[pair] = exchange.klines(pair)
|
for pair in pairs:
|
||||||
|
tickers[pair] = exchange.klines((pair, tick_interval))
|
||||||
else:
|
else:
|
||||||
tickers = history.load_data(
|
tickers = history.load_data(
|
||||||
datadir=Path(_CONF.get("datadir")),
|
datadir=Path(_CONF.get("datadir")),
|
||||||
pairs=[pair],
|
pairs=pairs,
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
refresh_pairs=_CONF.get('refresh_pairs', False),
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
exchange=Exchange(_CONF)
|
exchange=Exchange(_CONF)
|
||||||
)
|
)
|
||||||
|
|
||||||
# No ticker found, or impossible to download
|
# No ticker found, impossible to download, len mismatch
|
||||||
if tickers == {}:
|
for pair, data in tickers.copy().items():
|
||||||
exit()
|
logger.debug("checking tickers data of pair: %s", pair)
|
||||||
|
logger.debug("data.empty: %s", data.empty)
|
||||||
|
logger.debug("len(data): %s", len(data))
|
||||||
|
if data.empty:
|
||||||
|
del tickers[pair]
|
||||||
|
logger.info(
|
||||||
|
'An issue occured while retreiving datas of %s pair, please retry '
|
||||||
|
'using -l option for live or --refresh-pairs-cached', pair)
|
||||||
|
return tickers
|
||||||
|
|
||||||
# Get trades already made from the DB
|
|
||||||
trades = load_trades(args, pair, timerange)
|
def generate_dataframe(strategy, tickers, pair) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Get tickers then Populate strategy indicators and signals, then return the full dataframe
|
||||||
|
:return: the DataFrame of a pair
|
||||||
|
"""
|
||||||
|
|
||||||
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||||
|
|
||||||
dataframe = dataframes[pair]
|
dataframe = dataframes[pair]
|
||||||
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
||||||
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
||||||
|
|
||||||
if len(dataframe.index) > args.plot_limit:
|
return dataframe
|
||||||
logger.warning('Ticker contained more than %s candles as defined '
|
|
||||||
'with --plot-limit, clipping.', args.plot_limit)
|
|
||||||
dataframe = dataframe.tail(args.plot_limit)
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_trades_of_period(dataframe, trades) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Compare trades and backtested pair DataFrames to get trades performed on backtested period
|
||||||
|
:return: the DataFrame of a trades of period
|
||||||
|
"""
|
||||||
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
|
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
|
||||||
fig = generate_graph(
|
return trades
|
||||||
pair=pair,
|
|
||||||
trades=trades,
|
|
||||||
data=dataframe,
|
|
||||||
args=args
|
|
||||||
)
|
|
||||||
|
|
||||||
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots:
|
def generate_graph(
|
||||||
|
pair: str,
|
||||||
|
trades: pd.DataFrame,
|
||||||
|
data: pd.DataFrame,
|
||||||
|
indicators1: str,
|
||||||
|
indicators2: str
|
||||||
|
) -> tools.make_subplots:
|
||||||
"""
|
"""
|
||||||
Generate the graph from the data generated by Backtesting or from DB
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
:param pair: Pair to Display on the graph
|
:param pair: Pair to Display on the graph
|
||||||
:param trades: All trades created
|
:param trades: All trades created
|
||||||
:param data: Dataframe
|
:param data: Dataframe
|
||||||
:param args: sys.argv that contrains the two params indicators1, and indicators2
|
:indicators1: String Main plot indicators
|
||||||
|
:indicators2: String Sub plot indicators
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -201,6 +239,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
fig['layout']['yaxis2'].update(title='Volume')
|
fig['layout']['yaxis2'].update(title='Volume')
|
||||||
fig['layout']['yaxis3'].update(title='Other')
|
fig['layout']['yaxis3'].update(title='Other')
|
||||||
|
fig['layout']['xaxis']['rangeslider'].update(visible=False)
|
||||||
|
|
||||||
# Common information
|
# Common information
|
||||||
candles = go.Candlestick(
|
candles = go.Candlestick(
|
||||||
|
@ -285,7 +324,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
|
||||||
fig.append_trace(bb_lower, 1, 1)
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
fig.append_trace(bb_upper, 1, 1)
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
|
|
||||||
fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data)
|
fig = generate_row(fig=fig, row=1, raw_indicators=indicators1, data=data)
|
||||||
fig.append_trace(buys, 1, 1)
|
fig.append_trace(buys, 1, 1)
|
||||||
fig.append_trace(sells, 1, 1)
|
fig.append_trace(sells, 1, 1)
|
||||||
fig.append_trace(trade_buys, 1, 1)
|
fig.append_trace(trade_buys, 1, 1)
|
||||||
|
@ -300,7 +339,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool
|
||||||
fig.append_trace(volume, 2, 1)
|
fig.append_trace(volume, 2, 1)
|
||||||
|
|
||||||
# Row 3
|
# Row 3
|
||||||
fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data)
|
fig = generate_row(fig=fig, row=3, raw_indicators=indicators2, data=data)
|
||||||
|
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
@ -349,7 +388,7 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
help='Set indicators from your strategy you want in the third row of the graph. Separate '
|
help='Set indicators from your strategy you want in the third row of the graph. Separate '
|
||||||
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
|
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
|
||||||
type=str,
|
type=str,
|
||||||
default='macd',
|
default='macd,macdsignal',
|
||||||
dest='indicators2',
|
dest='indicators2',
|
||||||
)
|
)
|
||||||
arguments.parser.add_argument(
|
arguments.parser.add_argument(
|
||||||
|
@ -366,15 +405,58 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
return arguments.parse_args()
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def analyse_and_plot_pairs(args: Namespace):
|
||||||
|
"""
|
||||||
|
From arguments provided in cli:
|
||||||
|
-Initialise backtest env
|
||||||
|
-Get tickers data
|
||||||
|
-Generate Dafaframes populated with indicators and signals
|
||||||
|
-Load trades excecuted on same periods
|
||||||
|
-Generate Plotly plot objects
|
||||||
|
-Generate plot files
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
strategy, exchange, pairs = get_trading_env(args)
|
||||||
|
# Set timerange to use
|
||||||
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
tick_interval = strategy.ticker_interval
|
||||||
|
|
||||||
|
tickers = get_tickers_data(strategy, exchange, pairs, args)
|
||||||
|
pair_counter = 0
|
||||||
|
for pair, data in tickers.items():
|
||||||
|
pair_counter += 1
|
||||||
|
logger.info("analyse pair %s", pair)
|
||||||
|
tickers = {}
|
||||||
|
tickers[pair] = data
|
||||||
|
dataframe = generate_dataframe(strategy, tickers, pair)
|
||||||
|
|
||||||
|
trades = load_trades(args, pair, timerange)
|
||||||
|
trades = extract_trades_of_period(dataframe, trades)
|
||||||
|
|
||||||
|
fig = generate_graph(
|
||||||
|
pair=pair,
|
||||||
|
trades=trades,
|
||||||
|
data=dataframe,
|
||||||
|
indicators1=args.indicators1,
|
||||||
|
indicators2=args.indicators2
|
||||||
|
)
|
||||||
|
|
||||||
|
is_last = (False, True)[pair_counter == len(tickers)]
|
||||||
|
generate_plot_file(fig, pair, tick_interval, is_last)
|
||||||
|
|
||||||
|
logger.info('End of ploting process %s plots generated', pair_counter)
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv: List[str]) -> None:
|
def main(sysargv: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
This function will initiate the bot and start the trading loop.
|
This function will initiate the bot and start the trading loop.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logger.info('Starting Plot Dataframe')
|
logger.info('Starting Plot Dataframe')
|
||||||
plot_analyzed_dataframe(
|
analyse_and_plot_pairs(
|
||||||
plot_parse_args(sysargv)
|
plot_parse_args(sysargv)
|
||||||
)
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -29,6 +29,7 @@ from freqtrade.configuration import Configuration
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.state import RunMode
|
||||||
import freqtrade.misc as misc
|
import freqtrade.misc as misc
|
||||||
|
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ def plot_profit(args: Namespace) -> None:
|
||||||
# to match the tickerdata against the profits-results
|
# to match the tickerdata against the profits-results
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
config = Configuration(args).get_config()
|
config = Configuration(args, RunMode.OTHER).get_config()
|
||||||
|
|
||||||
# Init strategy
|
# Init strategy
|
||||||
try:
|
try:
|
||||||
|
@ -107,8 +108,8 @@ def plot_profit(args: Namespace) -> None:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
# Take pairs from the cli otherwise switch to the pair in the config file
|
# Take pairs from the cli otherwise switch to the pair in the config file
|
||||||
if args.pair:
|
if args.pairs:
|
||||||
filter_pairs = args.pair
|
filter_pairs = args.pairs
|
||||||
filter_pairs = filter_pairs.split(',')
|
filter_pairs = filter_pairs.split(',')
|
||||||
else:
|
else:
|
||||||
filter_pairs = config['exchange']['pair_whitelist']
|
filter_pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -11,7 +11,7 @@ from freqtrade import __version__
|
||||||
|
|
||||||
setup(name='freqtrade',
|
setup(name='freqtrade',
|
||||||
version=__version__,
|
version=__version__,
|
||||||
description='Simple High Frequency Trading Bot for crypto currencies',
|
description='Crypto Trading Bot',
|
||||||
url='https://github.com/freqtrade/freqtrade',
|
url='https://github.com/freqtrade/freqtrade',
|
||||||
author='gcarq and contributors',
|
author='gcarq and contributors',
|
||||||
author_email='michael.egger@tsn.at',
|
author_email='michael.egger@tsn.at',
|
||||||
|
@ -38,7 +38,7 @@ setup(name='freqtrade',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'coinmarketcap',
|
'coinmarketcap',
|
||||||
'scikit-optimize',
|
'scikit-optimize',
|
||||||
'ujson',
|
'python-rapidjson',
|
||||||
'py_find_1st'
|
'py_find_1st'
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
|
67
setup.sh
67
setup.sh
|
@ -17,23 +17,27 @@ function check_installed_python() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -z ${PYTHON} ]; then
|
||||||
|
echo "No usable python found. Please make sure to have python3.6 or python3.7 installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateenv () {
|
function updateenv() {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Update your virtual env"
|
echo "Updating your virtual env"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
source .env/bin/activate
|
source .env/bin/activate
|
||||||
echo "pip3 install in-progress. Please wait..."
|
echo "pip3 install in-progress. Please wait..."
|
||||||
pip3 install --quiet --upgrade pip
|
# Install numpy first to have py_find_1st install clean
|
||||||
pip3 install --quiet -r requirements.txt --upgrade
|
pip3 install --upgrade pip numpy
|
||||||
pip3 install --quiet -r requirements.txt
|
pip3 install --upgrade -r requirements.txt
|
||||||
|
|
||||||
read -p "Do you want to install dependencies for dev [Y/N]? "
|
read -p "Do you want to install dependencies for dev [y/N]? "
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
pip3 install --quiet -r requirements-dev.txt --upgrade
|
pip3 install --upgrade -r requirements-dev.txt
|
||||||
pip3 install --quiet -r requirements-dev.txt
|
|
||||||
else
|
else
|
||||||
echo "Dev dependencies ignored."
|
echo "Dev dependencies ignored."
|
||||||
fi
|
fi
|
||||||
|
@ -44,7 +48,13 @@ function updateenv () {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
function install_talib () {
|
function install_talib() {
|
||||||
|
if [ -f /usr/local/lib/libta_lib.a ]; then
|
||||||
|
echo "ta-lib already installed, skipping"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd build_helpers
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib
|
cd ta-lib
|
||||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||||
|
@ -52,14 +62,15 @@ function install_talib () {
|
||||||
make
|
make
|
||||||
sudo make install
|
sudo make install
|
||||||
cd .. && rm -rf ./ta-lib/
|
cd .. && rm -rf ./ta-lib/
|
||||||
|
cd ..
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install bot MacOS
|
# Install bot MacOS
|
||||||
function install_macos () {
|
function install_macos() {
|
||||||
if [ ! -x "$(command -v brew)" ]
|
if [ ! -x "$(command -v brew)" ]
|
||||||
then
|
then
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Install Brew"
|
echo "Installing Brew"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
|
@ -69,7 +80,7 @@ function install_macos () {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install bot Debian_ubuntu
|
# Install bot Debian_ubuntu
|
||||||
function install_debian () {
|
function install_debian() {
|
||||||
sudo add-apt-repository ppa:jonathonf/python-3.6
|
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||||
sudo apt-get update
|
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
|
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
|
# Upgrade the bot
|
||||||
function update () {
|
function update() {
|
||||||
git pull
|
git pull
|
||||||
updateenv
|
updateenv
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset Develop or Master branch
|
# Reset Develop or Master branch
|
||||||
function reset () {
|
function reset() {
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
echo "Reset branch and virtual env"
|
echo "Reseting branch and virtual env"
|
||||||
echo "----------------------------"
|
echo "----------------------------"
|
||||||
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
||||||
then
|
then
|
||||||
|
@ -127,11 +138,11 @@ function test_and_fix_python_on_mac() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function config_generator () {
|
function config_generator() {
|
||||||
|
|
||||||
echo "Starting to generate config.json"
|
echo "Starting to generate config.json"
|
||||||
echo
|
echo
|
||||||
echo "General configuration"
|
echo "Generating General configuration"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
default_max_trades=3
|
default_max_trades=3
|
||||||
read -p "Max open trades: (Default: $default_max_trades) " max_trades
|
read -p "Max open trades: (Default: $default_max_trades) " max_trades
|
||||||
|
@ -150,13 +161,13 @@ function config_generator () {
|
||||||
fiat_currency=${fiat_currency:-$default_fiat_currency}
|
fiat_currency=${fiat_currency:-$default_fiat_currency}
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Exchange config generator"
|
echo "Generating exchange config "
|
||||||
echo "------------------------"
|
echo "------------------------"
|
||||||
read -p "Exchange API key: " api_key
|
read -p "Exchange API key: " api_key
|
||||||
read -p "Exchange API Secret: " api_secret
|
read -p "Exchange API Secret: " api_secret
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Telegram config generator"
|
echo "Generating Telegram config"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
read -p "Telegram Token: " token
|
read -p "Telegram Token: " token
|
||||||
read -p "Telegram Chat_id: " chat_id
|
read -p "Telegram Chat_id: " chat_id
|
||||||
|
@ -173,14 +184,14 @@ function config_generator () {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function config () {
|
function config() {
|
||||||
|
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Config file generator"
|
echo "Generating config file"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
if [ -f config.json ]
|
if [ -f config.json ]
|
||||||
then
|
then
|
||||||
read -p "A config file already exist, do you want to override it [Y/N]? "
|
read -p "A config file already exist, do you want to override it [y/N]? "
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
then
|
then
|
||||||
config_generator
|
config_generator
|
||||||
|
@ -199,9 +210,9 @@ function config () {
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
function install () {
|
function install() {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Install mandatory dependencies"
|
echo "Installing mandatory dependencies"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
|
|
||||||
if [ "$(uname -s)" == "Darwin" ]
|
if [ "$(uname -s)" == "Darwin" ]
|
||||||
|
@ -222,21 +233,21 @@ function install () {
|
||||||
reset
|
reset
|
||||||
config
|
config
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Run the bot"
|
echo "Run the bot !"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
|
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
|
||||||
}
|
}
|
||||||
|
|
||||||
function plot () {
|
function plot() {
|
||||||
echo "
|
echo "
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
Install dependencies for Plotting scripts
|
Installing dependencies for Plotting scripts
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
"
|
"
|
||||||
pip install plotly --upgrade
|
pip install plotly --upgrade
|
||||||
}
|
}
|
||||||
|
|
||||||
function help () {
|
function help() {
|
||||||
echo "usage:"
|
echo "usage:"
|
||||||
echo " -i,--install Install freqtrade from scratch"
|
echo " -i,--install Install freqtrade from scratch"
|
||||||
echo " -u,--update Command git pull to update."
|
echo " -u,--update Command git pull to update."
|
||||||
|
|
|
@ -42,6 +42,7 @@ class SampleHyperOpts(IHyperOpt):
|
||||||
# Bollinger bands
|
# Bollinger bands
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@ -66,6 +67,7 @@ class SampleHyperOpts(IHyperOpt):
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||||
|
|
||||||
# TRIGGERS
|
# TRIGGERS
|
||||||
|
if 'trigger' in params:
|
||||||
if params['trigger'] == 'bb_lower':
|
if params['trigger'] == 'bb_lower':
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
|
@ -102,6 +104,67 @@ class SampleHyperOpts(IHyperOpt):
|
||||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Define the sell strategy parameters to be used by hyperopt
|
||||||
|
"""
|
||||||
|
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Sell strategy Hyperopt will build and use
|
||||||
|
"""
|
||||||
|
# print(params)
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
|
||||||
|
conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
|
||||||
|
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
|
||||||
|
conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
|
||||||
|
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
|
||||||
|
conditions.append(dataframe['adx'] < params['sell-adx-value'])
|
||||||
|
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
|
||||||
|
|
||||||
|
# TRIGGERS
|
||||||
|
if 'sell-trigger' in params:
|
||||||
|
if params['sell-trigger'] == 'sell-bb_upper':
|
||||||
|
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
|
||||||
|
if params['sell-trigger'] == 'sell-macd_cross_signal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['macdsignal'], dataframe['macd']
|
||||||
|
))
|
||||||
|
if params['sell-trigger'] == 'sell-sar_reversal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['sar'], dataframe['close']
|
||||||
|
))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
return populate_sell_trend
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sell_indicator_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Define your Hyperopt space for searching sell strategy parameters
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(75, 100, name='sell-mfi-value'),
|
||||||
|
Integer(50, 100, name='sell-fastd-value'),
|
||||||
|
Integer(50, 100, name='sell-adx-value'),
|
||||||
|
Integer(60, 100, name='sell-rsi-value'),
|
||||||
|
Categorical([True, False], name='sell-mfi-enabled'),
|
||||||
|
Categorical([True, False], name='sell-fastd-enabled'),
|
||||||
|
Categorical([True, False], name='sell-adx-enabled'),
|
||||||
|
Categorical([True, False], name='sell-rsi-enabled'),
|
||||||
|
Categorical(['sell-bb_upper',
|
||||||
|
'sell-macd_cross_signal',
|
||||||
|
'sell-sar_reversal'], name='sell-trigger')
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
|
@ -137,3 +200,36 @@ class SampleHyperOpts(IHyperOpt):
|
||||||
Real(0.01, 0.07, name='roi_p2'),
|
Real(0.01, 0.07, name='roi_p2'),
|
||||||
Real(0.01, 0.20, name='roi_p3'),
|
Real(0.01, 0.20, name='roi_p3'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators. Should be a copy of from strategy
|
||||||
|
must align to populate_indicators in this file
|
||||||
|
Only used when --spaces does not include buy
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['close'] < dataframe['bb_lowerband']) &
|
||||||
|
(dataframe['mfi'] < 16) &
|
||||||
|
(dataframe['adx'] > 25) &
|
||||||
|
(dataframe['rsi'] < 21)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators. Should be a copy of from strategy
|
||||||
|
must align to populate_indicators in this file
|
||||||
|
Only used when --spaces does not include sell
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(
|
||||||
|
dataframe['macdsignal'], dataframe['macd']
|
||||||
|
)) &
|
||||||
|
(dataframe['fastd'] > 54)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
||||||
|
|
|
@ -42,12 +42,22 @@ class TestStrategy(IStrategy):
|
||||||
# This attribute will be overridden if the config file contains "stoploss"
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
|
|
||||||
|
# trailing stoploss
|
||||||
|
trailing_stop = False
|
||||||
|
trailing_stop_positive = 0.01
|
||||||
|
trailing_stop_positive_offset = 0.0 # Disabled / not configured
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = '5m'
|
ticker_interval = '5m'
|
||||||
|
|
||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
ta_on_candle = False
|
ta_on_candle = False
|
||||||
|
|
||||||
|
# Experimental settings (configuration will overide these if set)
|
||||||
|
use_sell_signal = False
|
||||||
|
sell_profit_only = False
|
||||||
|
ignore_roi_if_buy_signal = False
|
||||||
|
|
||||||
# Optional order type mapping
|
# Optional order type mapping
|
||||||
order_types = {
|
order_types = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
|
@ -62,6 +72,19 @@ class TestStrategy(IStrategy):
|
||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def informative_pairs(self):
|
||||||
|
"""
|
||||||
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
These pair/interval combinations are non-tradeable, unless they are part
|
||||||
|
of the whitelist as well.
|
||||||
|
For more information, please consult the documentation
|
||||||
|
:return: List of tuples in the format (pair, interval)
|
||||||
|
Sample: return [("ETH/USDT", "5m"),
|
||||||
|
("BTC/USDT", "15m"),
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
@ -243,7 +266,8 @@ class TestStrategy(IStrategy):
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 30) &
|
(dataframe['adx'] > 30) &
|
||||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
(dataframe['tema'] > dataframe['tema'].shift(1)) &
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
|
|
||||||
|
@ -260,7 +284,8 @@ class TestStrategy(IStrategy):
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 70) &
|
(dataframe['adx'] > 70) &
|
||||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
(dataframe['tema'] < dataframe['tema'].shift(1)) &
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
Loading…
Reference in New Issue
Block a user