Merge with develop
This commit is contained in:
commit
ce663f6af5
@ -18,7 +18,9 @@ install:
|
|||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
- script:
|
||||||
|
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
|
- coveralls
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
||||||
@ -27,8 +29,6 @@ jobs:
|
|||||||
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
||||||
- script: flake8 freqtrade
|
- script: flake8 freqtrade
|
||||||
- script: mypy freqtrade
|
- script: mypy freqtrade
|
||||||
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=
|
||||||
|
@ -42,6 +42,11 @@ pip3.6 install flake8 coveralls
|
|||||||
flake8 freqtrade
|
flake8 freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We receive a lot of code that fails the `flake8` checks.
|
||||||
|
To help with that, we encourage you to install the git pre-commit
|
||||||
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
|
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||||
|
|
||||||
## 3. Test if all type-hints are correct
|
## 3. Test if all type-hints are correct
|
||||||
|
|
||||||
**Install packages** (If not already installed)
|
**Install packages** (If not already installed)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Bot Optimization
|
# Bot 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
|
## Table of Contents
|
||||||
|
|
||||||
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
||||||
- [Customize your strategy](#change-your-strategy)
|
- [Customize your strategy](#change-your-strategy)
|
||||||
- [Add more Indicator](#add-more-indicator)
|
- [Add more Indicator](#add-more-indicator)
|
||||||
@ -11,10 +13,12 @@ indicators.
|
|||||||
Since the version `0.16.0` the bot allows using custom strategy file.
|
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
|
||||||
`user_data/strategies`.
|
`user_data/strategies`.
|
||||||
|
|
||||||
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
||||||
|
|
||||||
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
||||||
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
||||||
|
|
||||||
@ -23,12 +27,14 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Change your strategy
|
## Change your strategy
|
||||||
|
|
||||||
The bot includes a default strategy file. However, we recommend you to
|
The bot includes a default strategy file. However, we recommend you to
|
||||||
use your own file to not have to lose your parameters every time the default
|
use your own file to not have to lose your parameters every time the default
|
||||||
strategy file will be updated on Github. Put your custom strategy file
|
strategy file will be updated on Github. Put your custom strategy file
|
||||||
into the folder `user_data/strategies`.
|
into the folder `user_data/strategies`.
|
||||||
|
|
||||||
A strategy file contains all the information needed to build a good strategy:
|
A strategy file contains all the information needed to build a good strategy:
|
||||||
|
|
||||||
- Buy strategy rules
|
- Buy strategy rules
|
||||||
- Sell strategy rules
|
- Sell strategy rules
|
||||||
- Minimal ROI recommended
|
- Minimal ROI recommended
|
||||||
@ -38,11 +44,12 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specify custom strategy location
|
### Specify custom strategy location
|
||||||
|
|
||||||
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -53,10 +60,12 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
|
|||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
### Buy strategy
|
### Buy strategy
|
||||||
|
|
||||||
Edit the method `populate_buy_trend()` into your strategy file to
|
Edit the method `populate_buy_trend()` into your strategy file to
|
||||||
update your buy strategy.
|
update your buy strategy.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -76,10 +85,11 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Sell strategy
|
### Sell strategy
|
||||||
Edit the method `populate_sell_trend()` into your strategy file to
|
|
||||||
update your sell strategy.
|
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -98,11 +108,13 @@ def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Add more Indicator
|
## Add more Indicator
|
||||||
|
|
||||||
As you have seen, buy and sell strategies need indicators. You can add
|
As you have seen, buy and sell strategies need indicators. You can add
|
||||||
more indicators by extending the list contained in
|
more indicators by extending the list contained in
|
||||||
the method `populate_indicators()` from your strategy file.
|
the method `populate_indicators()` from your strategy file.
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -137,16 +149,25 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
**Want more indicators example?**
|
### 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).
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||||
Then uncomment indicators you need.
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
|
||||||
### 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
|
||||||
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||||
|
|
||||||
|
### Further strategy ideas
|
||||||
|
|
||||||
|
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
|
||||||
|
Feel free to use any of them as inspiration for your own strategies.
|
||||||
|
We're happy to accept Pull Requests containing new Strategies to that repo.
|
||||||
|
|
||||||
|
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have a perfect strategy you probably want to backtesting 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](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md).
|
||||||
|
@ -16,7 +16,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
|
|||||||
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
||||||
* [Windows](#windows)
|
* [Windows](#windows)
|
||||||
|
|
||||||
|
|
||||||
<!-- /TOC -->
|
<!-- /TOC -->
|
||||||
|
|
||||||
------
|
------
|
||||||
@ -35,7 +34,9 @@ usage:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### --install
|
### --install
|
||||||
|
|
||||||
This script will install everything you need to run the bot:
|
This script will install everything you need to run the bot:
|
||||||
|
|
||||||
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
||||||
* Setup your virtualenv
|
* Setup your virtualenv
|
||||||
* Configure your `config.json` file
|
* Configure your `config.json` file
|
||||||
@ -43,12 +44,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`.
|
||||||
|
|
||||||
------
|
------
|
||||||
@ -63,7 +67,6 @@ Start by downloading Docker for your platform:
|
|||||||
|
|
||||||
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
||||||
|
|
||||||
|
|
||||||
### 1. Prepare the Bot
|
### 1. Prepare the Bot
|
||||||
|
|
||||||
#### 1.1. Clone the git repository
|
#### 1.1. Clone the git repository
|
||||||
@ -95,16 +98,17 @@ cp -n config.json.example config.json
|
|||||||
#### 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
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
touch tradesv3.sqlite
|
touch tradesv3.sqlite
|
||||||
````
|
````
|
||||||
|
|
||||||
Dry-Run
|
Dry-Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
touch tradesv3.dryrun.sqlite
|
touch tradesv3.dryrun.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 2. Build the Docker image
|
### 2. Build the Docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -114,7 +118,6 @@ docker build -t freqtrade .
|
|||||||
|
|
||||||
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
||||||
|
|
||||||
|
|
||||||
### 3. Verify the Docker image
|
### 3. Verify the Docker image
|
||||||
|
|
||||||
After the build process you can verify that the image was created with:
|
After the build process you can verify that the image was created with:
|
||||||
@ -123,7 +126,6 @@ After the build process you can verify that the image was created with:
|
|||||||
docker images
|
docker images
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 4. Run the Docker image
|
### 4. Run the Docker image
|
||||||
|
|
||||||
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
||||||
@ -137,11 +139,11 @@ There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtim
|
|||||||
```bash
|
```bash
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
### 5. Run a restartable docker image
|
### 5. Run a restartable docker image
|
||||||
|
|
||||||
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).
|
||||||
@ -164,6 +166,7 @@ docker run -d \
|
|||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
freqtrade --db-url sqlite:///tradesv3.sqlite
|
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||||
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||||
|
|
||||||
@ -190,13 +193,13 @@ We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windo
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
Click each one for install guide:
|
Click each one for install guide:
|
||||||
|
|
||||||
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
|
|
||||||
|
|
||||||
### Linux - Ubuntu 16.04
|
### Linux - Ubuntu 16.04
|
||||||
|
|
||||||
#### 1. Install Python 3.6, Git, and wget
|
#### 1. Install Python 3.6, Git, and wget
|
||||||
@ -240,17 +243,12 @@ Clone the git repository:
|
|||||||
git clone https://github.com/freqtrade/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout develop
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Configure `freqtrade` as a `systemd` service
|
#### 5. Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
|
||||||
After that you can start the daemon with:
|
After that you can start the daemon with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user start freqtrade
|
systemctl --user start freqtrade
|
||||||
```
|
```
|
||||||
@ -261,7 +259,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
|
|||||||
sudo loginctl enable-linger "$USER"
|
sudo loginctl enable-linger "$USER"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
#### 1. Install Python 3.6, git, wget and ta-lib
|
#### 1. Install Python 3.6, git, wget and ta-lib
|
||||||
@ -296,7 +293,6 @@ Optionally checkout the develop branch:
|
|||||||
git checkout develop
|
git checkout develop
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Setup Config and virtual env
|
### Setup Config and virtual env
|
||||||
|
|
||||||
#### 1. Initialize the configuration
|
#### 1. Initialize the configuration
|
||||||
@ -308,7 +304,6 @@ 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](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
||||||
|
|
||||||
|
|
||||||
#### 2. Setup your Python virtual environment (virtualenv)
|
#### 2. Setup your Python virtual environment (virtualenv)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -331,27 +326,41 @@ python3.6 ./freqtrade/main.py -c config.json
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
We recommend that Windows users use [Docker](#docker) as this will work
|
We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure).
|
||||||
much easier and smoother (also more secure).
|
|
||||||
|
|
||||||
### Install freqtrade
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
|
If that is not available on your system, feel free to try the instructions below, which led to success for some.
|
||||||
|
|
||||||
|
### Install freqtrade manually
|
||||||
|
|
||||||
|
#### Clone the git repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||||
|
|
||||||
|
#### install ta-lib
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
>python -m venv .env
|
>python -m venv .env
|
||||||
>cd .env\Scripts
|
>cd .env\Scripts
|
||||||
>activate.bat
|
>activate.bat
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
|
REM optionally install ta-lib from wheel
|
||||||
|
REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl
|
||||||
>pip install -r requirements.txt
|
>pip install -r requirements.txt
|
||||||
>pip install -e .
|
>pip install -e .
|
||||||
>cd freqtrade
|
>python freqtrade\main.py
|
||||||
>python main.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||||
|
|
||||||
|
|
||||||
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](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
||||||
|
@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | | Stops the trader
|
||||||
|
| `/reload_conf` | | Reloads the configuration file
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format
|
| `/status table` | | List all open trades in a table format
|
||||||
| `/count` | | Displays number of trades used and available
|
| `/count` | | Displays number of trades used and available
|
||||||
|
@ -62,6 +62,7 @@ class Analyze(object):
|
|||||||
'close': 'last',
|
'close': 'last',
|
||||||
'volume': 'max',
|
'volume': 'max',
|
||||||
})
|
})
|
||||||
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
@ -72,7 +72,7 @@ class Arguments(object):
|
|||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
action='version',
|
action='version',
|
||||||
version='%(prog)s {}'.format(__version__),
|
version=f'%(prog)s {__version__}'
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-c', '--config',
|
'-c', '--config',
|
||||||
@ -165,6 +165,11 @@ class Arguments(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given common arguments for Backtesting and Hyperopt scripts.
|
||||||
|
:param parser:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-i', '--ticker-interval',
|
'-i', '--ticker-interval',
|
||||||
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
||||||
@ -243,7 +248,7 @@ class Arguments(object):
|
|||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if text is None:
|
if text is None:
|
||||||
return TimeRange()
|
return TimeRange(None, None, 0, 0)
|
||||||
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
(r'^(\d{8})-$', ('date', None)),
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
@ -296,28 +301,34 @@ class Arguments(object):
|
|||||||
'--pairs-file',
|
'--pairs-file',
|
||||||
help='File containing a list of pairs to download',
|
help='File containing a list of pairs to download',
|
||||||
dest='pairs_file',
|
dest='pairs_file',
|
||||||
default=None
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--export',
|
'--export',
|
||||||
help='Export files to given dir',
|
help='Export files to given dir',
|
||||||
dest='export',
|
dest='export',
|
||||||
default=None)
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--days',
|
'--days',
|
||||||
help='Download data for number of days',
|
help='Download data for number of days',
|
||||||
dest='days',
|
dest='days',
|
||||||
type=int,
|
type=int,
|
||||||
default=None)
|
metavar='INT',
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--exchange',
|
'--exchange',
|
||||||
help='Exchange name (default: %(default)s)',
|
help='Exchange name (default: %(default)s)',
|
||||||
dest='exchange',
|
dest='exchange',
|
||||||
type=str,
|
type=str,
|
||||||
default='bittrex')
|
default='bittrex'
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-t', '--timeframes',
|
'-t', '--timeframes',
|
||||||
|
@ -61,11 +61,9 @@ class Configuration(object):
|
|||||||
with open(path) as file:
|
with open(path) as file:
|
||||||
conf = json.load(file)
|
conf = json.load(file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.critical(
|
raise OperationalException(
|
||||||
'Config file "%s" not found. Please create your config file',
|
'Config file "{}" not found!'
|
||||||
path
|
' Please create a config file or check whether it exists.'.format(path))
|
||||||
)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
if 'internals' not in conf:
|
if 'internals' not in conf:
|
||||||
conf['internals'] = {}
|
conf['internals'] = {}
|
||||||
@ -238,9 +236,8 @@ class Configuration(object):
|
|||||||
exchange = config.get('exchange', {}).get('name').lower()
|
exchange = config.get('exchange', {}).get('name').lower()
|
||||||
if exchange not in ccxt.exchanges:
|
if exchange not in ccxt.exchanges:
|
||||||
|
|
||||||
exception_msg = 'Exchange "{}" not supported.\n' \
|
exception_msg = f'Exchange "{exchange}" not supported.\n' \
|
||||||
'The following exchanges are supported: {}'\
|
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
|
||||||
.format(exchange, ', '.join(ccxt.exchanges))
|
|
||||||
|
|
||||||
logger.critical(exception_msg)
|
logger.critical(exception_msg)
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -59,7 +59,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
|||||||
name = exchange_config['name']
|
name = exchange_config['name']
|
||||||
|
|
||||||
if name not in ccxt.exchanges:
|
if name not in ccxt.exchanges:
|
||||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
try:
|
try:
|
||||||
api = getattr(ccxt, name.lower())({
|
api = getattr(ccxt, name.lower())({
|
||||||
'apiKey': exchange_config.get('key'),
|
'apiKey': exchange_config.get('key'),
|
||||||
@ -69,7 +69,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
|
|||||||
'enableRateLimit': True,
|
'enableRateLimit': True,
|
||||||
})
|
})
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@ -118,11 +118,10 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
# TODO: add a support for having coins in BTC/USDT format
|
# TODO: add a support for having coins in BTC/USDT format
|
||||||
if not pair.endswith(stake_cur):
|
if not pair.endswith(stake_cur):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||||
)
|
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} is not available at {}'.format(pair, get_name()))
|
f'Pair {pair} is not available at {get_name()}')
|
||||||
|
|
||||||
|
|
||||||
def exchange_has(endpoint: str) -> bool:
|
def exchange_has(endpoint: str) -> bool:
|
||||||
@ -138,7 +137,7 @@ def exchange_has(endpoint: str) -> bool:
|
|||||||
def buy(pair: str, rate: float, amount: float) -> Dict:
|
def buy(pair: str, rate: float, amount: float) -> Dict:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
@ -156,20 +155,17 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
return _API.create_limit_buy_order(pair, amount, rate)
|
return _API.create_limit_buy_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Insufficient funds to create limit buy order on market {}.'
|
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||||
'Tried to buy amount {} at rate {} (total {}).'
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not create limit buy order on market {}.'
|
f'Could not create limit buy order on market {pair}.'
|
||||||
'Tried to buy amount {} at rate {} (total {}).'
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not place buy order due to {}. Message: {}'.format(
|
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -177,7 +173,7 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
def sell(pair: str, rate: float, amount: float) -> Dict:
|
def sell(pair: str, rate: float, amount: float) -> Dict:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
@ -194,20 +190,17 @@ def sell(pair: str, rate: float, amount: float) -> Dict:
|
|||||||
return _API.create_limit_sell_order(pair, amount, rate)
|
return _API.create_limit_sell_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Insufficient funds to create limit sell order on market {}.'
|
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||||
'Tried to sell amount {} at rate {} (total {}).'
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not create limit sell order on market {}.'
|
f'Could not create limit sell order on market {pair}.'
|
||||||
'Tried to sell amount {} at rate {} (total {}).'
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
'Message: {}'.format(pair, amount, rate, rate*amount, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not place sell order due to {}. Message: {}'.format(
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -222,8 +215,7 @@ def get_balance(currency: str) -> float:
|
|||||||
balance = balances.get(currency)
|
balance = balances.get(currency)
|
||||||
if balance is None:
|
if balance is None:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get {} balance due to malformed exchange response: {}'.format(
|
f'Could not get {currency} balance due to malformed exchange response: {balances}')
|
||||||
currency, balances))
|
|
||||||
return balance['free']
|
return balance['free']
|
||||||
|
|
||||||
|
|
||||||
@ -243,8 +235,7 @@ def get_balances() -> dict:
|
|||||||
return balances
|
return balances
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get balance due to {}. Message: {}'.format(
|
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -255,13 +246,11 @@ def get_tickers() -> Dict:
|
|||||||
return _API.fetch_tickers()
|
return _API.fetch_tickers()
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Exchange {} does not support fetching tickers in batch.'
|
f'Exchange {_API.name} does not support fetching tickers in batch.'
|
||||||
'Message: {}'.format(_API.name, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load tickers due to {}. Message: {}'.format(
|
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -277,13 +266,12 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
|||||||
'bid': float(data['bid']),
|
'bid': float(data['bid']),
|
||||||
'ask': float(data['ask']),
|
'ask': float(data['ask']),
|
||||||
}
|
}
|
||||||
except KeyError as e:
|
except KeyError:
|
||||||
logger.debug("Could not cache ticker data for %s", pair)
|
logger.debug("Could not cache ticker data for %s", pair)
|
||||||
return data
|
return data
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load ticker history due to {}. Message: {}'.format(
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
else:
|
else:
|
||||||
@ -327,15 +315,13 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
|||||||
return data
|
return data
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Exchange {} does not support fetching historical candlestick data.'
|
f'Exchange {_API.name} does not support fetching historical candlestick data.'
|
||||||
'Message: {}'.format(_API.name, e)
|
f'Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load ticker history due to {}. Message: {}'.format(
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
|
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||||
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
@ -347,12 +333,10 @@ def cancel_order(order_id: str, pair: str) -> None:
|
|||||||
return _API.cancel_order(order_id, pair)
|
return _API.cancel_order(order_id, pair)
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not cancel order. Message: {}'.format(e)
|
f'Could not cancel order. Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not cancel order due to {}. Message: {}'.format(
|
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -369,12 +353,10 @@ def get_order(order_id: str, pair: str) -> Dict:
|
|||||||
return _API.fetch_order(order_id, pair)
|
return _API.fetch_order(order_id, pair)
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
'Could not get order. Message: {}'.format(e)
|
f'Could not get order. Message: {e}')
|
||||||
)
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get order due to {}. Message: {}'.format(
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -393,8 +375,7 @@ def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
|
|||||||
|
|
||||||
except ccxt.NetworkError as e:
|
except ccxt.NetworkError as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get trades due to networking error. Message: {}'.format(e)
|
f'Could not get trades due to networking error. Message: {e}')
|
||||||
)
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -416,8 +397,7 @@ def get_markets() -> List[dict]:
|
|||||||
return _API.fetch_markets()
|
return _API.fetch_markets()
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not load markets due to {}. Message: {}'.format(
|
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
@ -442,8 +422,7 @@ def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
|
|||||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
'Could not get fee info due to {}. Message: {}'.format(
|
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||||
e.__class__.__name__, e))
|
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class CryptoToFiatConverter(object):
|
|||||||
|
|
||||||
# Check if the fiat convertion you want is supported
|
# Check if the fiat convertion you want is supported
|
||||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||||
|
|
||||||
# Get the pair that interest us and return the price in fiat
|
# Get the pair that interest us and return the price in fiat
|
||||||
for pair in self._pairs:
|
for pair in self._pairs:
|
||||||
@ -182,7 +182,7 @@ class CryptoToFiatConverter(object):
|
|||||||
"""
|
"""
|
||||||
# Check if the fiat convertion you want is supported
|
# Check if the fiat convertion you want is supported
|
||||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
|
||||||
|
|
||||||
# No need to convert if both crypto and fiat are the same
|
# No need to convert if both crypto and fiat are the same
|
||||||
if crypto_symbol == fiat_symbol:
|
if crypto_symbol == fiat_symbol:
|
||||||
@ -192,6 +192,7 @@ class CryptoToFiatConverter(object):
|
|||||||
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||||
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return float(
|
return float(
|
||||||
self._coinmarketcap.ticker(
|
self._coinmarketcap.ticker(
|
||||||
|
@ -76,17 +76,14 @@ class FreqtradeBot(object):
|
|||||||
else:
|
else:
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
|
|
||||||
def clean(self) -> bool:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Cleanup the application state und finish all pending tasks
|
Cleanup pending resources on an already stopped bot
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.rpc.send_msg('*Status:* `Stopping trader...`')
|
logger.info('Cleaning up modules ...')
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
|
||||||
self.state = State.STOPPED
|
|
||||||
self.rpc.cleanup()
|
self.rpc.cleanup()
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
return True
|
|
||||||
|
|
||||||
def worker(self, old_state: State = None) -> State:
|
def worker(self, old_state: State = None) -> State:
|
||||||
"""
|
"""
|
||||||
@ -97,7 +94,7 @@ class FreqtradeBot(object):
|
|||||||
# Log state transition
|
# Log state transition
|
||||||
state = self.state
|
state = self.state
|
||||||
if state != old_state:
|
if state != old_state:
|
||||||
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
|
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
@ -171,12 +168,10 @@ class FreqtradeBot(object):
|
|||||||
logger.warning('%s, retrying in 30 seconds...', error)
|
logger.warning('%s, retrying in 30 seconds...', error)
|
||||||
time.sleep(constants.RETRY_TIMEOUT)
|
time.sleep(constants.RETRY_TIMEOUT)
|
||||||
except OperationalException:
|
except OperationalException:
|
||||||
|
tb = traceback.format_exc()
|
||||||
|
hint = 'Issue `/start` if you think it is safe to restart.'
|
||||||
self.rpc.send_msg(
|
self.rpc.send_msg(
|
||||||
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
|
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
||||||
.format(
|
|
||||||
traceback=traceback.format_exc(),
|
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
logger.exception('OperationalException. Stopping trader ...')
|
logger.exception('OperationalException. Stopping trader ...')
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
@ -275,8 +270,6 @@ class FreqtradeBot(object):
|
|||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
|
||||||
:param interval: Ticker interval used for Analyze
|
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
"""
|
"""
|
||||||
interval = self.analyze.get_ticker_interval()
|
interval = self.analyze.get_ticker_interval()
|
||||||
@ -284,6 +277,9 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
|
stake_currency = self.config['stake_currency']
|
||||||
|
fiat_currency = self.config['fiat_display_currency']
|
||||||
|
exc_name = exchange.get_name()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
@ -308,7 +304,8 @@ class FreqtradeBot(object):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
pair_s = pair.replace('_', '/')
|
||||||
|
pair_url = exchange.get_pair_detail_url(pair)
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
||||||
amount = stake_amount / buy_limit
|
amount = stake_amount / buy_limit
|
||||||
@ -317,23 +314,15 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
stake_amount_fiat = self.fiat_converter.convert_amount(
|
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||||
stake_amount,
|
stake_amount,
|
||||||
self.config['stake_currency'],
|
stake_currency,
|
||||||
self.config['fiat_display_currency']
|
fiat_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
self.rpc.send_msg(
|
self.rpc.send_msg(
|
||||||
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
|
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
||||||
.format(
|
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
||||||
exchange.get_name(),
|
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
||||||
pair.replace('_', '/'),
|
|
||||||
exchange.get_pair_detail_url(pair),
|
|
||||||
buy_limit,
|
|
||||||
stake_amount,
|
|
||||||
self.config['stake_currency'],
|
|
||||||
stake_amount_fiat,
|
|
||||||
self.config['fiat_display_currency']
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
@ -434,12 +423,12 @@ class FreqtradeBot(object):
|
|||||||
fee_abs += exectrade['fee']['cost']
|
fee_abs += exectrade['fee']['cost']
|
||||||
|
|
||||||
if amount != order_amount:
|
if amount != order_amount:
|
||||||
logger.warning("amount {} does not match amount {}".format(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("Applying fee on amount for {} (from {} to {}) from Trades".format(
|
logger.info(f"""Applying fee on amount for {trade} \
|
||||||
trade, order['amount'], real_amount))
|
(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:
|
||||||
@ -448,7 +437,7 @@ 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('attempt to handle closed trade: {}'.format(trade))
|
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
@ -474,6 +463,12 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
try:
|
try:
|
||||||
|
# FIXME: Somehow the query above returns results
|
||||||
|
# where the open_order_id is in fact None.
|
||||||
|
# This is probably because the record got
|
||||||
|
# updated via /forcesell in a different thread.
|
||||||
|
if not trade.open_order_id:
|
||||||
|
continue
|
||||||
order = exchange.get_order(trade.open_order_id, trade.pair)
|
order = exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -499,16 +494,14 @@ class FreqtradeBot(object):
|
|||||||
"""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('_', '/')
|
||||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
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)
|
Trade.session.delete(trade)
|
||||||
# FIX? do we really need to flush, caller of
|
|
||||||
# check_handle_timedout will flush afterwards
|
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
logger.info('Buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
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
|
||||||
@ -517,8 +510,7 @@ class FreqtradeBot(object):
|
|||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
logger.info('Partial buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||||
@ -527,6 +519,7 @@ class FreqtradeBot(object):
|
|||||||
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
|
||||||
exchange.cancel_order(trade.open_order_id, trade.pair)
|
exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
@ -535,8 +528,7 @@ class FreqtradeBot(object):
|
|||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
||||||
trade.pair.replace('_', '/')))
|
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -550,6 +542,8 @@ class FreqtradeBot(object):
|
|||||||
:param limit: limit rate for the sell order
|
:param limit: limit rate for the sell order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
exc = trade.exchange
|
||||||
|
pair = trade.pair
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
@ -559,43 +553,31 @@ class FreqtradeBot(object):
|
|||||||
profit_trade = trade.calc_profit(rate=limit)
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = trade.calc_profit_percent(limit)
|
profit = trade.calc_profit_percent(limit)
|
||||||
|
pair_url = exchange.get_pair_detail_url(trade.pair)
|
||||||
|
gain = "profit" if fmt_exp_profit > 0 else "loss"
|
||||||
|
|
||||||
message = "*{exchange}:* Selling\n" \
|
message = f"*{exc}:* Selling\n" \
|
||||||
"*Current Pair:* [{pair}]({pair_url})\n" \
|
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||||
"*Limit:* `{limit}`\n" \
|
f"*Limit:* `{limit}`\n" \
|
||||||
"*Amount:* `{amount}`\n" \
|
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
"*Profit:* `{profit:.2f}%`" \
|
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
||||||
"".format(
|
""
|
||||||
exchange=trade.exchange,
|
|
||||||
pair=trade.pair,
|
|
||||||
pair_url=exchange.get_pair_detail_url(trade.pair),
|
|
||||||
limit=limit,
|
|
||||||
open_rate=trade.open_rate,
|
|
||||||
current_rate=current_rate,
|
|
||||||
amount=round(trade.amount, 8),
|
|
||||||
profit=round(profit * 100, 2),
|
|
||||||
)
|
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# For regular case, when the configuration exists
|
||||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
fiat = self.config['fiat_display_currency']
|
||||||
fiat_converter = CryptoToFiatConverter()
|
fiat_converter = CryptoToFiatConverter()
|
||||||
profit_fiat = fiat_converter.convert_amount(
|
profit_fiat = fiat_converter.convert_amount(
|
||||||
profit_trade,
|
profit_trade,
|
||||||
self.config['stake_currency'],
|
stake,
|
||||||
self.config['fiat_display_currency']
|
fiat
|
||||||
)
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
|
||||||
'` / {profit_fiat:.3f} {fiat})`' \
|
|
||||||
''.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade,
|
|
||||||
coin=self.config['stake_currency'],
|
|
||||||
profit_fiat=profit_fiat,
|
|
||||||
fiat=self.config['fiat_display_currency'],
|
|
||||||
)
|
)
|
||||||
|
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||||
|
f'` / {profit_fiat:.3f} {fiat})`'\
|
||||||
|
''
|
||||||
# Because telegram._forcesell does not have the configuration
|
# Because telegram._forcesell does not have the configuration
|
||||||
# Ignore the FIAT value and does not show the stake_currency as well
|
# Ignore the FIAT value and does not show the stake_currency as well
|
||||||
else:
|
else:
|
||||||
|
@ -5,12 +5,14 @@ Read the documentation to know what cli arguments you need.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from argparse import Namespace
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
@ -44,6 +46,8 @@ def main(sysargv: List[str]) -> None:
|
|||||||
state = None
|
state = None
|
||||||
while 1:
|
while 1:
|
||||||
state = freqtrade.worker(old_state=state)
|
state = freqtrade.worker(old_state=state)
|
||||||
|
if state == State.RELOAD_CONF:
|
||||||
|
freqtrade = reconfigure(freqtrade, args)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
@ -55,10 +59,28 @@ def main(sysargv: List[str]) -> None:
|
|||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
if freqtrade:
|
if freqtrade:
|
||||||
freqtrade.clean()
|
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
|
||||||
|
freqtrade.cleanup()
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
Cleans up current instance, reloads the configuration and returns the new instance
|
||||||
|
"""
|
||||||
|
# Clean up current modules
|
||||||
|
freqtrade.cleanup()
|
||||||
|
|
||||||
|
# Create new instance
|
||||||
|
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
||||||
|
freqtrade.rpc.send_msg(
|
||||||
|
'*Status:* `Config reloaded ...`'.format(
|
||||||
|
freqtrade.state.name.lower()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return freqtrade
|
||||||
|
|
||||||
|
|
||||||
def set_loggers() -> None:
|
def set_loggers() -> None:
|
||||||
"""
|
"""
|
||||||
Set the logger level for Third party libs
|
Set the logger level for Third party libs
|
||||||
|
@ -85,7 +85,7 @@ def load_data(datadir: str,
|
|||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
pairs: Optional[List[str]] = None,
|
pairs: Optional[List[str]] = None,
|
||||||
refresh_pairs: Optional[bool] = False,
|
refresh_pairs: Optional[bool] = False,
|
||||||
timerange: TimeRange = TimeRange()) -> Dict[str, List]:
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Loads ticker history data for the given parameters
|
||||||
:return: dict
|
:return: dict
|
||||||
@ -125,7 +125,7 @@ def make_testdata_path(datadir: str) -> str:
|
|||||||
|
|
||||||
def download_pairs(datadir, pairs: List[str],
|
def download_pairs(datadir, pairs: List[str],
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
timerange: TimeRange = TimeRange()) -> bool:
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
|
||||||
"""For each pairs passed in parameters, download the ticker intervals"""
|
"""For each pairs passed in parameters, download the ticker intervals"""
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
try:
|
try:
|
||||||
|
@ -299,6 +299,11 @@ class RPC(object):
|
|||||||
|
|
||||||
return True, '*Status:* `already stopped`'
|
return True, '*Status:* `already stopped`'
|
||||||
|
|
||||||
|
def rpc_reload_conf(self) -> str:
|
||||||
|
""" Handler for reload_conf. """
|
||||||
|
self.freqtrade.state = State.RELOAD_CONF
|
||||||
|
return '*Status:* `Reloading config ...`'
|
||||||
|
|
||||||
# FIX: no test for this!!!!
|
# FIX: no test for this!!!!
|
||||||
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
|
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
|
||||||
"""
|
"""
|
||||||
@ -354,6 +359,7 @@ class RPC(object):
|
|||||||
return True, 'Invalid argument.'
|
return True, 'Invalid argument.'
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
|
Trade.session.flush()
|
||||||
return False, ''
|
return False, ''
|
||||||
|
|
||||||
def rpc_performance(self) -> Tuple[bool, Any]:
|
def rpc_performance(self) -> Tuple[bool, Any]:
|
||||||
|
@ -93,6 +93,7 @@ class Telegram(RPC):
|
|||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
|
CommandHandler('reload_conf', self._reload_conf),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
CommandHandler('version', self._version),
|
||||||
]
|
]
|
||||||
@ -300,6 +301,18 @@ class Telegram(RPC):
|
|||||||
(error, msg) = self.rpc_stop()
|
(error, msg) = self.rpc_stop()
|
||||||
self.send_msg(msg, bot=bot)
|
self.send_msg(msg, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /reload_conf.
|
||||||
|
Triggers a config file reload
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
msg = self.rpc_reload_conf()
|
||||||
|
self.send_msg(msg, bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -8,7 +8,8 @@ import enum
|
|||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
"""
|
"""
|
||||||
Bot running states
|
Bot application states
|
||||||
"""
|
"""
|
||||||
RUNNING = 0
|
RUNNING = 0
|
||||||
STOPPED = 1
|
STOPPED = 1
|
||||||
|
RELOAD_CONF = 2
|
||||||
|
@ -340,7 +340,7 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 100
|
assert len(data['UNITTEST/BTC']) == 99
|
||||||
|
|
||||||
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||||
analyze = Analyze(default_conf)
|
analyze = Analyze(default_conf)
|
||||||
@ -364,7 +364,7 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
|||||||
)
|
)
|
||||||
min_date, max_date = backtesting.get_timeframe(data)
|
min_date, max_date = backtesting.get_timeframe(data)
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_generate_text_table(default_conf, mocker):
|
||||||
@ -501,7 +501,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.optimize.backtesting.exchange.get_fee', fee)
|
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
tests = [['raise', 17], ['lower', 0], ['sine', 16]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
@ -629,10 +629,12 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--datadir', 'freqtrade/tests/testdata',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--timerange', '-100'
|
'--timerange', '-100',
|
||||||
|
'--realistic-simulation'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start(args)
|
||||||
@ -642,13 +644,14 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
'Using ticker_interval: 1m ...',
|
'Using ticker_interval: 1m ...',
|
||||||
'Parameter -l/--live detected ...',
|
'Parameter -l/--live detected ...',
|
||||||
'Using max_open_trades: 1 ...',
|
'Using max_open_trades: 1 ...',
|
||||||
'Parameter --timerange detected: -100 ..',
|
'Parameter --timerange detected: -100 ...',
|
||||||
'Using data folder: freqtrade/tests/testdata ...',
|
'Using data folder: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Downloading data for all pairs in whitelist ...',
|
'Downloading data for all pairs in whitelist ...',
|
||||||
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
|
'Parameter --realistic-simulation detected ...'
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
@ -70,12 +70,12 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.call_count == 0
|
assert start_polling.call_count == 0
|
||||||
|
|
||||||
# number of handles registered
|
# number of handles registered
|
||||||
assert start_polling.dispatcher.add_handler.call_count == 11
|
assert start_polling.dispatcher.add_handler.call_count > 0
|
||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
||||||
"['count'], ['help'], ['version']]"
|
"['count'], ['reload_conf'], ['help'], ['version']]"
|
||||||
|
|
||||||
assert log_has(message_str, caplog.record_tuples)
|
assert log_has(message_str, caplog.record_tuples)
|
||||||
|
|
||||||
@ -745,6 +745,29 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||||
|
""" Test _reload_conf() method """
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
send_msg=msg_mock
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
telegram._reload_conf(bot=MagicMock(), update=update)
|
||||||
|
assert freqtradebot.state == State.RELOAD_CONF
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
|
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _forcesell() method
|
Test _forcesell() method
|
||||||
|
@ -46,7 +46,7 @@ def test_analyze_object() -> None:
|
|||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
dataframe = Analyze.parse_ticker_dataframe(result)
|
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||||
assert len(result.index) == len(dataframe.index)
|
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
@ -188,4 +188,4 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
|||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 100
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||||
|
@ -174,3 +174,19 @@ def test_parse_args_hyperopt_custom() -> None:
|
|||||||
assert call_args.subparser == 'hyperopt'
|
assert call_args.subparser == 'hyperopt'
|
||||||
assert call_args.spaces == ['buy']
|
assert call_args.spaces == ['buy']
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdata_dl_options() -> None:
|
||||||
|
args = [
|
||||||
|
'--pairs-file', 'file_with_pairs',
|
||||||
|
'--export', 'export/folder',
|
||||||
|
'--days', '30',
|
||||||
|
'--exchange', 'binance'
|
||||||
|
]
|
||||||
|
arguments = Arguments(args, '')
|
||||||
|
arguments.testdata_dl_options()
|
||||||
|
args = arguments.parse_args()
|
||||||
|
assert args.pairs_file == 'file_with_pairs'
|
||||||
|
assert args.export == 'export/folder'
|
||||||
|
assert args.days == 30
|
||||||
|
assert args.exchange == 'binance'
|
||||||
|
@ -97,7 +97,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
|||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_file_exception(mocker, caplog) -> None:
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Configuration._load_config_file() method
|
Test Configuration._load_config_file() method
|
||||||
"""
|
"""
|
||||||
@ -107,12 +107,8 @@ def test_load_config_file_exception(mocker, caplog) -> None:
|
|||||||
)
|
)
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
|
||||||
configuration._load_config_file('somefile')
|
configuration._load_config_file('somefile')
|
||||||
assert log_has(
|
|
||||||
'Config file "somefile" not found. Please create your config file',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config(default_conf, mocker) -> None:
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
|
@ -9,7 +9,7 @@ import pytest
|
|||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
def test_pair_convertion_object():
|
def test_pair_convertion_object():
|
||||||
@ -90,6 +90,13 @@ def test_fiat_convert_find_price(mocker):
|
|||||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||||
|
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_get_price(mocker):
|
def test_fiat_convert_get_price(mocker):
|
||||||
api_mock = MagicMock(return_value={
|
api_mock = MagicMock(return_value={
|
||||||
'price_usd': 28000.0,
|
'price_usd': 28000.0,
|
||||||
@ -161,7 +168,8 @@ def test_fiat_init_network_exception(mocker):
|
|||||||
fiat_convert._cryptomap = {}
|
fiat_convert._cryptomap = {}
|
||||||
fiat_convert._load_cryptomap()
|
fiat_convert._load_cryptomap()
|
||||||
|
|
||||||
assert len(fiat_convert._cryptomap) == 0
|
length_cryptomap = len(fiat_convert._cryptomap)
|
||||||
|
assert length_cryptomap == 0
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_without_network():
|
def test_fiat_convert_without_network():
|
||||||
@ -175,3 +183,22 @@ def test_fiat_convert_without_network():
|
|||||||
assert fiat_convert._coinmarketcap is None
|
assert fiat_convert._coinmarketcap is None
|
||||||
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
||||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_amount(mocker):
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||||
|
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
result = fiat_convert.convert_amount(
|
||||||
|
crypto_amount=1.23,
|
||||||
|
crypto_symbol="BTC",
|
||||||
|
fiat_symbol="USD"
|
||||||
|
)
|
||||||
|
assert result == 15184.35
|
||||||
|
|
||||||
|
result = fiat_convert.convert_amount(
|
||||||
|
crypto_amount=1.23,
|
||||||
|
crypto_symbol="BTC",
|
||||||
|
fiat_symbol="BTC"
|
||||||
|
)
|
||||||
|
assert result == 1.23
|
||||||
|
@ -68,7 +68,7 @@ def test_freqtradebot_object() -> None:
|
|||||||
Test the FreqtradeBot object has the mandatory public methods
|
Test the FreqtradeBot object has the mandatory public methods
|
||||||
"""
|
"""
|
||||||
assert hasattr(FreqtradeBot, 'worker')
|
assert hasattr(FreqtradeBot, 'worker')
|
||||||
assert hasattr(FreqtradeBot, 'clean')
|
assert hasattr(FreqtradeBot, 'cleanup')
|
||||||
assert hasattr(FreqtradeBot, 'create_trade')
|
assert hasattr(FreqtradeBot, 'create_trade')
|
||||||
assert hasattr(FreqtradeBot, 'get_target_bid')
|
assert hasattr(FreqtradeBot, 'get_target_bid')
|
||||||
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
|
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
|
||||||
@ -93,7 +93,7 @@ def test_freqtradebot(mocker, default_conf) -> None:
|
|||||||
assert freqtrade.state is State.STOPPED
|
assert freqtrade.state is State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_clean(mocker, default_conf, caplog) -> None:
|
def test_cleanup(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test clean() method
|
Test clean() method
|
||||||
"""
|
"""
|
||||||
@ -101,11 +101,8 @@ def test_clean(mocker, default_conf, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
assert freqtrade.state == State.RUNNING
|
freqtrade.cleanup()
|
||||||
|
assert log_has('Cleaning up modules ...', caplog.record_tuples)
|
||||||
assert freqtrade.clean()
|
|
||||||
assert freqtrade.state == State.STOPPED
|
|
||||||
assert log_has('Stopping trader and cleaning up modules...', caplog.record_tuples)
|
|
||||||
assert mock_cleanup.call_count == 1
|
assert mock_cleanup.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -682,8 +679,10 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
|||||||
trade.open_fee = 0.001
|
trade.open_fee = 0.001
|
||||||
assert not freqtrade.process_maybe_execute_sell(trade)
|
assert not freqtrade.process_maybe_execute_sell(trade)
|
||||||
# Test amount not modified by fee-logic
|
# Test amount not modified by fee-logic
|
||||||
assert not log_has('Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(
|
assert not log_has(
|
||||||
trade), caplog.record_tuples)
|
'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
|
||||||
# test amount modified by fee-logic
|
# test amount modified by fee-logic
|
||||||
@ -694,6 +693,38 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
|
|||||||
# Assert we call handle_trade() if trade is feasible for execution
|
# Assert we call handle_trade() if trade is feasible for execution
|
||||||
assert freqtrade.process_maybe_execute_sell(trade)
|
assert freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
regexp = re.compile('Found open order for.*')
|
||||||
|
assert filter(regexp.match, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_maybe_execute_sell_exception(mocker, default_conf,
|
||||||
|
limit_buy_order, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test the exceptions in process_maybe_execute_sell()
|
||||||
|
"""
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order)
|
||||||
|
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.open_order_id = '123'
|
||||||
|
trade.open_fee = 0.001
|
||||||
|
|
||||||
|
# Test raise of OperationalException exception
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
side_effect=OperationalException()
|
||||||
|
)
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert log_has('could not update trade amount: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
# Test raise of DependencyException exception
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
side_effect=DependencyException()
|
||||||
|
)
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert log_has('Unable to sell trade: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
@ -1468,7 +1499,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
|||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
|
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker):
|
||||||
"""
|
"""
|
||||||
Test get_real_amount - fees in Stake currency
|
Test get_real_amount - fees in Stake currency
|
||||||
"""
|
"""
|
||||||
@ -1620,3 +1651,28 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
|
|||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
# Amount does not change
|
# Amount does not change
|
||||||
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_real_amount_open_trade(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open'
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker)
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
|
amount = 12345
|
||||||
|
trade = Trade(
|
||||||
|
pair='LTC/ETH',
|
||||||
|
amount=amount,
|
||||||
|
exchange='binance',
|
||||||
|
open_rate=0.245441,
|
||||||
|
open_order_id="123456"
|
||||||
|
)
|
||||||
|
order = {
|
||||||
|
'id': 'mocked_order',
|
||||||
|
'amount': amount,
|
||||||
|
'status': 'open',
|
||||||
|
}
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
assert freqtrade.get_real_amount(trade, order) == amount
|
||||||
|
@ -3,11 +3,16 @@ Unit test file for main.py
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.main import main, set_loggers
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.main import main, set_loggers, reconfigure
|
||||||
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +65,7 @@ def test_set_loggers() -> None:
|
|||||||
assert value2 is logging.INFO
|
assert value2 is logging.INFO
|
||||||
|
|
||||||
|
|
||||||
def test_main(mocker, caplog) -> None:
|
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test main() function
|
Test main() function
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
@ -68,26 +73,140 @@ def test_main(mocker, caplog) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
_init_modules=MagicMock(),
|
_init_modules=MagicMock(),
|
||||||
worker=MagicMock(
|
worker=MagicMock(side_effect=Exception),
|
||||||
side_effect=KeyboardInterrupt
|
cleanup=MagicMock(),
|
||||||
),
|
|
||||||
clean=MagicMock(),
|
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
args = ['-c', 'config.json.example']
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
# Test Main + the KeyboardInterrupt exception
|
# Test Main + the KeyboardInterrupt exception
|
||||||
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
|
||||||
main(args)
|
|
||||||
log_has('Starting freqtrade', caplog.record_tuples)
|
|
||||||
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
|
|
||||||
assert pytest_wrapped_e.type == SystemExit
|
|
||||||
assert pytest_wrapped_e.value.code == 42
|
|
||||||
|
|
||||||
# Test the BaseException case
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.freqtradebot.FreqtradeBot.worker',
|
|
||||||
MagicMock(side_effect=BaseException)
|
|
||||||
)
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
main(args)
|
main(args)
|
||||||
log_has('Got fatal exception!', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('Fatal exception!', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=KeyboardInterrupt),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(args)
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
args = ['-c', 'config.json.example']
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(args)
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
assert log_has('Oh snap!', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(return_value=State.RELOAD_CONF),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
# Raise exception as side effect to avoid endless loop
|
||||||
|
reconfigure_mock = mocker.patch(
|
||||||
|
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(['-c', 'config.json.example'])
|
||||||
|
|
||||||
|
assert reconfigure_mock.call_count == 1
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconfigure(mocker, default_conf) -> None:
|
||||||
|
""" Test recreate() function """
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
# Renew mock to return modified data
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['stake_amount'] += 1
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
# reconfigure should return a new instance
|
||||||
|
freqtrade2 = reconfigure(
|
||||||
|
freqtrade,
|
||||||
|
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify we have a new instance with the new config
|
||||||
|
assert freqtrade is not freqtrade2
|
||||||
|
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']
|
||||||
|
@ -39,7 +39,7 @@ def test_datesarray_to_datetimearray(ticker_history):
|
|||||||
assert dates[0].minute == 50
|
assert dates[0].minute == 50
|
||||||
|
|
||||||
date_len = len(dates)
|
date_len = len(dates)
|
||||||
assert date_len == 3
|
assert date_len == 2
|
||||||
|
|
||||||
|
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib && ./configure && make && sudo make install && cd ..
|
cd ta-lib && ./configure && make && sudo make install && cd ..
|
||||||
else
|
else
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ccxt==1.14.155
|
ccxt==1.14.172
|
||||||
SQLAlchemy==1.2.8
|
SQLAlchemy==1.2.8
|
||||||
python-telegram-bot==10.1.0
|
python-telegram-bot==10.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
|
@ -93,7 +93,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
config.get('strategy')
|
config.get('strategy')
|
||||||
)
|
)
|
||||||
exit(0)
|
exit(1)
|
||||||
|
|
||||||
# Load the profits results
|
# Load the profits results
|
||||||
try:
|
try:
|
||||||
@ -104,7 +104,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
logger.critical(
|
logger.critical(
|
||||||
'File "backtest-result.json" not found. This script require backtesting '
|
'File "backtest-result.json" not found. This script require backtesting '
|
||||||
'results to run.\nPlease run a backtesting with the parameter --export.')
|
'results to run.\nPlease run a backtesting with the parameter --export.')
|
||||||
exit(0)
|
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.pair:
|
||||||
|
@ -5,3 +5,6 @@ max-complexity = 12
|
|||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-freqtrade.tests.*]
|
||||||
|
ignore_errors = True
|
||||||
|
BIN
ta-lib-0.4.0-src.tar.gz
Normal file
BIN
ta-lib-0.4.0-src.tar.gz
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user