Drafting freqtrade 0.17.0 release
This commit is contained in:
commit
46a062d5fb
@ -3,3 +3,4 @@ omit =
|
|||||||
scripts/*
|
scripts/*
|
||||||
freqtrade/tests/*
|
freqtrade/tests/*
|
||||||
freqtrade/vendor/*
|
freqtrade/vendor/*
|
||||||
|
freqtrade/__main__.py
|
||||||
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@ -1,15 +1,17 @@
|
|||||||
## Step 1: Have you search for this issue before posting it?
|
## Step 1: Have you search for this issue before posting it?
|
||||||
|
|
||||||
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue).
|
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
|
||||||
If it hasn't been reported, please create a new issue.
|
If it hasn't been reported, please create a new issue.
|
||||||
|
|
||||||
## Step 2: Describe your environment
|
## Step 2: Describe your environment
|
||||||
|
|
||||||
* Python Version: _____ (`python -V`)
|
* Python Version: _____ (`python -V`)
|
||||||
|
* CCXT version: _____ (`pip freeze | grep ccxt`)
|
||||||
* Branch: Master | Develop
|
* Branch: Master | Develop
|
||||||
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
|
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
|
||||||
|
|
||||||
## Step 3: Describe the problem:
|
## Step 3: Describe the problem:
|
||||||
|
|
||||||
*Explain the problem you have encountered*
|
*Explain the problem you have encountered*
|
||||||
|
|
||||||
### Steps to reproduce:
|
### Steps to reproduce:
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,5 +1,5 @@
|
|||||||
Thank you for sending your pull request. But first, have you included
|
Thank you for sending your pull request. But first, have you included
|
||||||
unit tests, and is your code PEP8 conformant? [More details](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
Explain in one sentence the goal of this PR
|
Explain in one sentence the goal of this PR
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,13 +1,14 @@
|
|||||||
# Freqtrade rules
|
# Freqtrade rules
|
||||||
freqtrade/tests/testdata/*.json
|
freqtrade/tests/testdata/*.json
|
||||||
hyperopt_conf.py
|
hyperopt_conf.py
|
||||||
config.json
|
config*.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
.hyperopt
|
.hyperopt
|
||||||
logfile.txt
|
logfile.txt
|
||||||
hyperopt_trials.pickle
|
hyperopt_trials.pickle
|
||||||
user_data/
|
user_data/
|
||||||
freqtrade-plot.html
|
freqtrade-plot.html
|
||||||
|
freqtrade-profit-plot.html
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@ -90,3 +91,4 @@ target/
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
4
.pyup.yml
Normal file
4
.pyup.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# autogenerated pyup.io config file
|
||||||
|
# see https://pyup.io/docs/configuration/ for all available options
|
||||||
|
|
||||||
|
schedule: every day
|
13
.travis.yml
13
.travis.yml
@ -13,21 +13,22 @@ addons:
|
|||||||
install:
|
install:
|
||||||
- ./install_ta-lib.sh
|
- ./install_ta-lib.sh
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install --upgrade flake8 coveralls pytest-random-order
|
- pip install --upgrade flake8 coveralls pytest-random-order mypy
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- 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 backtesting
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py hyperopt -e 5
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
||||||
- script: flake8 freqtrade
|
- script: flake8 freqtrade
|
||||||
after_success:
|
- script: mypy freqtrade
|
||||||
- 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=
|
||||||
|
@ -7,7 +7,7 @@ Feel like our bot is missing a feature? We welcome your pull requests! Few point
|
|||||||
conformant (max-line-length = 100).
|
conformant (max-line-length = 100).
|
||||||
|
|
||||||
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
||||||
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
|
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
|
||||||
|
|
||||||
|
|
||||||
**Before sending the PR:**
|
**Before sending the PR:**
|
||||||
@ -42,4 +42,21 @@ 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
|
||||||
|
|
||||||
|
**Install packages** (If not already installed)
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip3.6 install mypy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run mypy**
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
mypy freqtrade
|
||||||
|
```
|
||||||
|
218
README.md
218
README.md
@ -1,14 +1,14 @@
|
|||||||
# freqtrade
|
# freqtrade
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
|
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/gcarq/freqtrade/maintainability)
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies designed to
|
Simple High frequency trading bot for crypto currencies designed to
|
||||||
support multi exchanges and be controlled via Telegram.
|
support multi exchanges and be controlled via Telegram.
|
||||||
|
|
||||||
![freqtrade](https://raw.githubusercontent.com/gcarq/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
This software is for educational purposes only. Do not risk money which
|
This software is for educational purposes only. Do not risk money which
|
||||||
@ -22,33 +22,10 @@ expect.
|
|||||||
We strongly recommend you to have coding and Python knowledge. Do not
|
We strongly recommend you to have coding and Python knowledge. Do not
|
||||||
hesitate to read the source code and understand the mechanism of this bot.
|
hesitate to read the source code and understand the mechanism of this bot.
|
||||||
|
|
||||||
## Table of Contents
|
## Exchange marketplaces supported
|
||||||
- [Features](#features)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [Quick start](#quick-start)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
|
|
||||||
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
|
||||||
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
|
|
||||||
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
|
|
||||||
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
|
||||||
- [Support](#support)
|
|
||||||
- [Help](#help--slack)
|
|
||||||
- [Bugs](#bugs--issues)
|
|
||||||
- [Feature Requests](#feature-requests)
|
|
||||||
- [Pull Requests](#pull-requests)
|
|
||||||
- [Basic Usage](#basic-usage)
|
|
||||||
- [Bot commands](#bot-commands)
|
|
||||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [Min hardware required](#min-hardware-required)
|
|
||||||
- [Software requirements](#software-requirements)
|
|
||||||
|
|
||||||
## Branches
|
|
||||||
The project is currently setup in two main branches:
|
|
||||||
- `develop` - This branch has often new features, but might also cause
|
|
||||||
breaking changes.
|
|
||||||
- `master` - This branch contains the latest stable release. The bot
|
|
||||||
'should' be stable on this branch, and is generally well tested.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
||||||
@ -56,96 +33,68 @@ Windows, macOS and Linux
|
|||||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
- [x] **Persistence**: Persistence is achieved through sqlite
|
||||||
- [x] **Dry-run**: Run the bot without playing money.
|
- [x] **Dry-run**: Run the bot without playing money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
|
||||||
parameters with Hyperopts.
|
strategy parameters with real exchange data.
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
||||||
want to trade.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you
|
|
||||||
want to avoid.
|
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in
|
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
||||||
33 fiat.
|
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
||||||
- [x] **Daily summary of profit/loss**: Provide a daily summary
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
of your profit/loss.
|
|
||||||
- [x] **Performance status report**: Provide a performance status of
|
|
||||||
your current trades.
|
|
||||||
|
|
||||||
### Exchange supported
|
## Table of Contents
|
||||||
- [x] Bittrex
|
- [Quick start](#quick-start)
|
||||||
- [ ] Binance
|
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
- [ ] Others
|
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
|
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
|
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
- [Basic Usage](#basic-usage)
|
||||||
|
- [Bot commands](#bot-commands)
|
||||||
|
- [Telegram RPC commands](#telegram-rpc-commands)
|
||||||
|
- [Support](#support)
|
||||||
|
- [Help](#help--slack)
|
||||||
|
- [Bugs](#bugs--issues)
|
||||||
|
- [Feature Requests](#feature-requests)
|
||||||
|
- [Pull Requests](#pull-requests)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Min hardware required](#min-hardware-required)
|
||||||
|
- [Software requirements](#software-requirements)
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
This quick start section is a very short explanation on how to test the
|
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||||
bot in dry-run. We invite you to read the
|
|
||||||
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
|
||||||
to ensure you understand how the bot is working.
|
|
||||||
|
|
||||||
### Easy installation
|
|
||||||
The script below will install all dependencies and help you to configure the bot.
|
|
||||||
```bash
|
```bash
|
||||||
./setup.sh --install
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
```
|
|
||||||
|
|
||||||
### Manual installation
|
|
||||||
The following steps are made for Linux/MacOS environment
|
|
||||||
|
|
||||||
**1. Clone the repo**
|
|
||||||
```bash
|
|
||||||
git clone git@github.com:gcarq/freqtrade.git
|
|
||||||
git checkout develop
|
git checkout develop
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
|
./setup.sh --install
|
||||||
```
|
```
|
||||||
**2. Create the config file**
|
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
|
||||||
Switch `"dry_run": true,`
|
|
||||||
```bash
|
|
||||||
cp config.json.example config.json
|
|
||||||
vi config.json
|
|
||||||
```
|
|
||||||
**3. Build your docker image and run it**
|
|
||||||
```bash
|
|
||||||
docker build -t freqtrade .
|
|
||||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Help / Slack
|
## Documentation
|
||||||
For any questions not covered by the documentation or for further
|
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||||
information about the bot, we encourage you to join our slack channel.
|
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
|
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
|
- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
|
||||||
|
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||||
|
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
||||||
|
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||||
|
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
|
||||||
### [Bugs / Issues](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
|
||||||
If you discover a bug in the bot, please
|
|
||||||
[search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
|
||||||
first. If it hasn't been reported, please
|
|
||||||
[create a new issue](https://github.com/gcarq/freqtrade/issues/new) and
|
|
||||||
ensure you follow the template guide so that our team can assist you as
|
|
||||||
quickly as possible.
|
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/gcarq/freqtrade/labels/enhancement)
|
|
||||||
Have you a great idea to improve the bot you want to share? Please,
|
|
||||||
first search if this feature was not [already discussed](https://github.com/gcarq/freqtrade/labels/enhancement).
|
|
||||||
If it hasn't been requested, please
|
|
||||||
[create a new request](https://github.com/gcarq/freqtrade/issues/new)
|
|
||||||
and ensure you follow the template guide so that it does not get lost
|
|
||||||
in the bug reports.
|
|
||||||
|
|
||||||
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
|
||||||
Please read our
|
|
||||||
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
|
||||||
to understand the requirements before sending your pull-requests.
|
|
||||||
|
|
||||||
**Important:** Always create your PR against the `develop` branch, not
|
|
||||||
`master`.
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
### Bot commands
|
### Bot commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
|
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--dynamic-whitelist [INT]]
|
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||||
|
[--dry-run-db]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
@ -161,23 +110,24 @@ optional arguments:
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
--dry-run-db Force dry run to use a local DB
|
-d PATH, --datadir PATH
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
path to backtest data (default:
|
||||||
only if dry_run is enabled.
|
freqtrade/tests/testdata
|
||||||
--datadir PATH path to backtest data (default freqdata/tests/testdata
|
-s NAME, --strategy NAME
|
||||||
|
specify strategy class name (default: DefaultStrategy)
|
||||||
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (Default 20 currencies)
|
BaseVolume (Default 20 currencies)
|
||||||
|
--dry-run-db Force dry run to use a local DB
|
||||||
|
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||||
|
only if dry_run is enabled.
|
||||||
```
|
```
|
||||||
More details on:
|
|
||||||
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
|
||||||
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
|
||||||
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
Telegram is not mandatory. However, this is a great way to control your
|
Telegram is not mandatory. However, this is a great way to control your
|
||||||
bot. More details on our
|
bot. More details on our
|
||||||
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
|
|
||||||
- `/start`: Starts the trader
|
- `/start`: Starts the trader
|
||||||
- `/stop`: Stops the trader
|
- `/stop`: Stops the trader
|
||||||
@ -192,6 +142,48 @@ bot. More details on our
|
|||||||
- `/help`: Show help message
|
- `/help`: Show help message
|
||||||
- `/version`: Show version
|
- `/version`: Show version
|
||||||
|
|
||||||
|
|
||||||
|
## Development branches
|
||||||
|
The project is currently setup in two main branches:
|
||||||
|
- `develop` - This branch has often new features, but might also cause
|
||||||
|
breaking changes.
|
||||||
|
- `master` - This branch contains the latest stable release. The bot
|
||||||
|
'should' be stable on this branch, and is generally well tested.
|
||||||
|
|
||||||
|
|
||||||
|
## Support
|
||||||
|
### Help / Slack
|
||||||
|
For any questions not covered by the documentation or for further
|
||||||
|
information about the bot, we encourage you to join our slack channel.
|
||||||
|
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||||
|
|
||||||
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
If you discover a bug in the bot, please
|
||||||
|
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
first. If it hasn't been reported, please
|
||||||
|
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
|
||||||
|
ensure you follow the template guide so that our team can assist you as
|
||||||
|
quickly as possible.
|
||||||
|
|
||||||
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
|
Have you a great idea to improve the bot you want to share? Please,
|
||||||
|
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
||||||
|
If it hasn't been requested, please
|
||||||
|
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
|
||||||
|
and ensure you follow the template guide so that it does not get lost
|
||||||
|
in the bug reports.
|
||||||
|
|
||||||
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
|
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||||
|
Please read our
|
||||||
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
|
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||||
|
|
||||||
|
**Important:** Always create your PR against the `develop` branch, not
|
||||||
|
`master`.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Min hardware required
|
### Min hardware required
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
|
"ticker_interval" : "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": 600,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
@ -13,24 +14,25 @@
|
|||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC_ETH",
|
"ETH/BTC",
|
||||||
"BTC_LTC",
|
"LTC/BTC",
|
||||||
"BTC_ETC",
|
"ETC/BTC",
|
||||||
"BTC_DASH",
|
"DASH/BTC",
|
||||||
"BTC_ZEC",
|
"ZEC/BTC",
|
||||||
"BTC_XLM",
|
"XLM/BTC",
|
||||||
"BTC_NXT",
|
"NXT/BTC",
|
||||||
"BTC_POWR",
|
"POWR/BTC",
|
||||||
"BTC_ADA",
|
"ADA/BTC",
|
||||||
"BTC_XMR"
|
"XMR/BTC"
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"BTC_DOGE"
|
"DOGE/BTC"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
"sell_profit_only": false
|
"sell_profit_only": false,
|
||||||
|
"ignore_roi_if_buy_signal": false
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": 5,
|
"ticker_interval": "5m",
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
@ -21,30 +21,32 @@
|
|||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC_ETH",
|
"ETH/BTC",
|
||||||
"BTC_LTC",
|
"LTC/BTC",
|
||||||
"BTC_ETC",
|
"ETC/BTC",
|
||||||
"BTC_DASH",
|
"DASH/BTC",
|
||||||
"BTC_ZEC",
|
"ZEC/BTC",
|
||||||
"BTC_XLM",
|
"XLM/BTC",
|
||||||
"BTC_NXT",
|
"NXT/BTC",
|
||||||
"BTC_POWR",
|
"POWR/BTC",
|
||||||
"BTC_ADA",
|
"ADA/BTC",
|
||||||
"BTC_XMR"
|
"XMR/BTC"
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"BTC_DOGE"
|
"DOGE/BTC"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
"sell_profit_only": false
|
"sell_profit_only": false,
|
||||||
|
"ignore_roi_if_buy_signal": false
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "your_telegram_token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "your_telegram_chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
|
@ -1,137 +1,186 @@
|
|||||||
# Backtesting
|
# Backtesting
|
||||||
|
|
||||||
This page explains how to validate your strategy performance by using
|
This page explains how to validate your strategy performance by using
|
||||||
Backtesting.
|
Backtesting.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
||||||
- [Understand the backtesting result](#understand-the-backtesting-result)
|
- [Understand the backtesting result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies, you want to test it against
|
Now you have good Buy and Sell strategies, you want to test it against
|
||||||
real data. This is what we call
|
real data. This is what we call
|
||||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||||
|
|
||||||
|
|
||||||
Backtesting will use the crypto-currencies (pair) from your config file
|
Backtesting will use the crypto-currencies (pair) from your config file
|
||||||
and load static tickers located in
|
and load static tickers located in
|
||||||
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
|
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||||
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
||||||
already in the `testdata` folder, backtesting will download them
|
already in the `testdata` folder, backtesting will download them
|
||||||
automatically. Testdata files will not be updated until you specify it.
|
automatically. Testdata files will not be updated until you specify it.
|
||||||
|
|
||||||
The result of backtesting will confirm you if your bot as more chance to
|
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
|
||||||
make a profit than a loss.
|
|
||||||
|
|
||||||
|
|
||||||
The backtesting is very easy with freqtrade.
|
The backtesting is very easy with freqtrade.
|
||||||
|
|
||||||
### Run a backtesting against the currencies listed in your config file
|
### Run a backtesting against the currencies listed in your config file
|
||||||
**With 5 min tickers (Per default)**
|
#### With 5 min tickers (Per default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
||||||
```
|
```
|
||||||
|
|
||||||
**With 1 min tickers**
|
#### With 1 min tickers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
**Reload your testdata files**
|
#### Update cached pairs with the latest data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
||||||
```
|
```
|
||||||
|
|
||||||
**With live data (do not alter your testdata files)**
|
#### With live data (do not alter your testdata files)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||||
```
|
```
|
||||||
|
|
||||||
**Using a different on-disk ticker-data source**
|
#### Using a different on-disk ticker-data source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
**With a (custom) strategy file**
|
#### With a (custom) strategy file
|
||||||
```bash
|
|
||||||
python3 ./freqtrade/main.py -s currentstrategy backtesting
|
```bash
|
||||||
```
|
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
||||||
Where `-s currentstrategy` refers to a filename `currentstrategy.py` in `freqtrade/user_data/strategies`
|
```
|
||||||
|
|
||||||
|
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
||||||
|
|
||||||
|
#### Exporting trades to file
|
||||||
|
|
||||||
**Exporting trades to file**
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
**Running backtest with smaller testset**
|
#### Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running backtest with smaller testset
|
||||||
|
|
||||||
Use the `--timerange` argument to change how much of the testset
|
Use the `--timerange` argument to change how much of the testset
|
||||||
you want to use. The last N ticks/timeframes will be used.
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --timerange=-200
|
python3 ./freqtrade/main.py backtesting --timerange=-200
|
||||||
```
|
```
|
||||||
|
|
||||||
***Advanced use of timerange***
|
#### Advanced use of timerange
|
||||||
|
|
||||||
Doing `--timerange=-200` will get the last 200 timeframes
|
Doing `--timerange=-200` will get the last 200 timeframes
|
||||||
from your inputdata. You can also specify specific dates,
|
from your inputdata. You can also specify specific dates,
|
||||||
or a range span indexed by start and stop.
|
or a range span indexed by start and stop.
|
||||||
|
|
||||||
The full timerange specification:
|
The full timerange specification:
|
||||||
|
|
||||||
- Use last 123 tickframes of data: `--timerange=-123`
|
- Use last 123 tickframes of data: `--timerange=-123`
|
||||||
- Use first 123 tickframes of data: `--timerange=123-`
|
- Use first 123 tickframes of data: `--timerange=123-`
|
||||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||||
|
- Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||||
|
- Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||||
|
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||||
|
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||||
|
`--timerange=1527595200-1527618600`
|
||||||
|
|
||||||
|
#### Downloading new set of ticker data
|
||||||
|
|
||||||
Incoming feature, not implemented yet:
|
To download new set of backtesting ticker data, you can use a download script.
|
||||||
- `--timerange=-20180131`
|
|
||||||
- `--timerange=20180101-`
|
|
||||||
- `--timerange=20180101-20181231`
|
|
||||||
|
|
||||||
|
If you are using Binance for example:
|
||||||
|
|
||||||
**Update testdata directory**
|
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
||||||
To update your testdata directory, or download into another testdata directory:
|
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||||
```bash
|
|
||||||
mkdir -p user_data/data/testdata-20180113
|
|
||||||
cp freqtrade/tests/testdata/pairs.json user_data/data-20180113
|
|
||||||
cd user_data/data-20180113
|
|
||||||
```
|
|
||||||
|
|
||||||
Possibly edit pairs.json file to include/exclude pairs
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 freqtrade/tests/testdata/download_backtest_data.py -p pairs.json
|
mkdir -p user_data/data/binance
|
||||||
|
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will read your pairs.json file, and download ticker data
|
Then run:
|
||||||
into the current working directory.
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/download_backtest_data --exchange binance
|
||||||
|
```
|
||||||
|
|
||||||
For help about backtesting usage, please refer to
|
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||||
[Backtesting commands](#backtesting-commands).
|
|
||||||
|
- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`.
|
||||||
|
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
|
||||||
|
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
- To download ticker data for only 10 days, use `--days 10`.
|
||||||
|
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||||
|
|
||||||
|
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
||||||
|
|
||||||
## Understand the backtesting result
|
## Understand the backtesting result
|
||||||
|
|
||||||
The most important in the backtesting is to understand the result.
|
The most important in the backtesting is to understand the result.
|
||||||
|
|
||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
====================== BACKTESTING REPORT ================================
|
======================================== BACKTESTING REPORT =========================================
|
||||||
pair buy count avg profit % total profit BTC avg duration
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
-------- ----------- -------------- ------------------ --------------
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
BTC_ETH 56 -0.67 -0.00075455 62.3
|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
|
||||||
BTC_LTC 38 -0.48 -0.00036315 57.9
|
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
|
||||||
BTC_ETC 42 -1.15 -0.00096469 67.0
|
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
|
||||||
BTC_DASH 72 -0.62 -0.00089368 39.9
|
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
|
||||||
BTC_ZEC 45 -0.46 -0.00041387 63.2
|
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
|
||||||
BTC_XLM 24 -0.88 -0.00041846 47.7
|
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
|
||||||
BTC_NXT 24 0.68 0.00031833 40.2
|
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
|
||||||
BTC_POWR 35 0.98 0.00064887 45.3
|
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
|
||||||
BTC_ADA 43 -0.39 -0.00032292 55.0
|
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
|
||||||
BTC_XMR 40 -0.40 -0.00032181 47.4
|
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
|
||||||
|
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
|
||||||
|
====================================== LEFT OPEN TRADES REPORT ======================================
|
||||||
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
|
||||||
|
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
|
||||||
|
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
|
||||||
|
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
|
||||||
|
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
|
||||||
|
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
|
||||||
|
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
|
||||||
|
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The 1st table will contain all trades the bot made.
|
||||||
|
|
||||||
|
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
|
||||||
|
These trades are also included in the first table, but are extracted separately for clarity.
|
||||||
|
|
||||||
The last line will give you the overall performance of your strategy,
|
The last line will give you the overall performance of your strategy,
|
||||||
here:
|
here:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
TOTAL 419 -0.41 -0.00348593 52.9
|
||||||
```
|
```
|
||||||
@ -147,6 +196,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
|
|||||||
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
||||||
expect the bot to make more profit than 1% (because it will sell every
|
expect the bot to make more profit than 1% (because it will sell every
|
||||||
time a trade will reach 1%).
|
time a trade will reach 1%).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"0": 0.01
|
"0": 0.01
|
||||||
@ -159,6 +209,7 @@ profit. Hence, keep in mind that your performance is a mix of your
|
|||||||
strategies, your configuration, and the crypto-currency you have set up.
|
strategies, your configuration, and the crypto-currency you have set up.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
optimal parameters to use for your strategy?
|
optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
@ -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,25 +44,28 @@ 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
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
||||||
```
|
```
|
||||||
|
|
||||||
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
### 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/gcarq/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
|
|
||||||
[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
### 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.
|
|
||||||
Your next step is to learn [How to use the Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md).
|
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).
|
||||||
|
@ -9,9 +9,10 @@ it.
|
|||||||
|
|
||||||
## Bot commands
|
## Bot commands
|
||||||
```
|
```
|
||||||
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
|
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--dry-run-db]
|
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||||
{backtesting,hyperopt} ...
|
[--db-url PATH]
|
||||||
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
|
|
||||||
@ -26,17 +27,17 @@ optional arguments:
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-c PATH, --config PATH
|
-c PATH, --config PATH
|
||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
path to backtest data
|
||||||
-s NAME, --strategy NAME
|
-s NAME, --strategy NAME
|
||||||
specify strategy class name (default: DefaultStrategy)
|
specify strategy class name (default: DefaultStrategy)
|
||||||
--strategy-path PATH specify additional strategy lookup path
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
--dry-run-db Force dry run to use a local DB
|
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
|
||||||
only if dry_run is enabled.
|
|
||||||
--datadir PATH
|
|
||||||
path to backtest data (default freqdata/tests/testdata
|
|
||||||
--dynamic-whitelist [INT]
|
--dynamic-whitelist [INT]
|
||||||
dynamically generate and update whitelist based on 24h
|
dynamically generate and update whitelist based on 24h
|
||||||
BaseVolume (Default 20 currencies)
|
BaseVolume (default: 20)
|
||||||
|
--db-url PATH Override trades database URL, this is useful if
|
||||||
|
dry_run is enabled or in custom deployments (default:
|
||||||
|
sqlite:///tradesv3.sqlite)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different config file?
|
### How to use a different config file?
|
||||||
@ -66,7 +67,7 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
|||||||
If the bot does not find your strategy file, it will display in an error
|
If the bot does not find your strategy file, it will display in an error
|
||||||
message the reason (File not found, or errors in your code).
|
message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
### How to use --strategy-path?
|
### How to use --strategy-path?
|
||||||
This parameter allows you to add an additional strategy lookup path, which gets
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
@ -100,14 +101,14 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
|
|||||||
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||||
value (20).
|
value (20).
|
||||||
|
|
||||||
### How to use --dry-run-db?
|
### How to use --db-url?
|
||||||
When you run the bot in Dry-run mode, per default no transactions are
|
When you run the bot in Dry-run mode, per default no transactions are
|
||||||
stored in a database. If you want to store your bot actions in a DB
|
stored in a database. If you want to store your bot actions in a DB
|
||||||
using `--dry-run-db`. This command will use a separate database file
|
using `--db-url`. This can also be used to specify a custom database
|
||||||
`tradesv3.dry_run.sqlite`
|
in production mode. Example command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -c config.json --dry-run-db
|
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -116,21 +117,32 @@ python3 ./freqtrade/main.py -c config.json --dry-run-db
|
|||||||
Backtesting also uses the config specified via `-c/--config`.
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
|
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||||
[-r]
|
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
|
||||||
|
[--export-filename EXPORTFILENAME]
|
||||||
|
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-l, --live using live data
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
-i INT, --ticker-interval INT
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
specify ticker interval in minutes (default: 5)
|
|
||||||
--realistic-simulation
|
--realistic-simulation
|
||||||
uses max_open_trades from config to simulate real
|
uses max_open_trades from config to simulate real
|
||||||
world limitations
|
world limitations
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
|
-l, --live using live data
|
||||||
-r, --refresh-pairs-cached
|
-r, --refresh-pairs-cached
|
||||||
refresh the pairs files in tests/testdata with
|
refresh the pairs files in tests/testdata with the
|
||||||
the latest data from Bittrex. Use it if you want
|
latest data from the exchange. Use it if you want to
|
||||||
to run your backtesting with up-to-date data.
|
run your backtesting with up-to-date data.
|
||||||
|
--export EXPORT export backtest results, argument are: trades Example
|
||||||
|
--export=trades
|
||||||
|
--export-filename EXPORTFILENAME
|
||||||
|
Save backtest results to this filename requires
|
||||||
|
--export to be set as well Example --export-
|
||||||
|
filename=backtest_today.json (default: backtest-
|
||||||
|
result.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use --refresh-pairs-cached parameter?
|
### How to use --refresh-pairs-cached parameter?
|
||||||
@ -148,26 +160,33 @@ the parameter `-l` or `--live`.
|
|||||||
|
|
||||||
## Hyperopt commands
|
## Hyperopt commands
|
||||||
|
|
||||||
It is possible to use hyperopt for trading strategy optimization.
|
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||||
Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
|
to find optimal parameter values for your stategy.
|
||||||
located in `freqtrade/optimize/hyperopt_conf.py`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb]
|
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||||
|
[--timerange TIMERANGE] [-e INT]
|
||||||
|
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
|
--realistic-simulation
|
||||||
|
uses max_open_trades from config to simulate real
|
||||||
|
world limitations
|
||||||
|
--timerange TIMERANGE specify what timerange of data to use.
|
||||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||||
--use-mongodb parallelize evaluations with mongodb (requires mongod
|
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
||||||
in PATH)
|
Specify which parameters to hyperopt. Space separate
|
||||||
|
list. Default: all
|
||||||
```
|
```
|
||||||
|
|
||||||
## A parameter missing in the configuration?
|
## A parameter missing in the configuration?
|
||||||
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
||||||
in [misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
The optimal strategy of the bot will change with time depending of the
|
The optimal strategy of the bot will change with time depending of the
|
||||||
market trends. The next step is to
|
market trends. The next step is to
|
||||||
[optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
@ -16,31 +16,40 @@ The table below will list all configuration parameters.
|
|||||||
|----------|---------|----------|-------------|
|
|----------|---------|----------|-------------|
|
||||||
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
|
||||||
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
||||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
|
||||||
| `ticker_interval` | [1, 5, 30, 60, 1440] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes
|
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
||||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||||
|
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
|
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
||||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||||
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
||||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||||
|
|
||||||
The definition of each config parameters is in
|
The definition of each config parameters is in
|
||||||
[misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||||
|
|
||||||
|
### Understand stake_amount
|
||||||
|
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
||||||
|
The minimal value is 0.0005. If there is not enough crypto-currency in
|
||||||
|
the account an exception is generated.
|
||||||
|
To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`.
|
||||||
|
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
@ -73,15 +82,35 @@ value. This parameter is optional. If you use it, it will take over the
|
|||||||
Possible values are `running` or `stopped`. (default=`running`)
|
Possible values are `running` or `stopped`. (default=`running`)
|
||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
|
|
||||||
|
### Understand process_throttle_secs
|
||||||
|
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||||
|
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
||||||
|
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
||||||
|
the static list of pairs) if we should buy.
|
||||||
|
|
||||||
### Understand ask_last_balance
|
### Understand ask_last_balance
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||||
use the `last` price and values between those interpolate between ask and last
|
use the `last` price and values between those interpolate between ask and last
|
||||||
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||||
end up paying more then would probably have been necessary.
|
end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
|
### What values for exchange.name?
|
||||||
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
|
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||||
|
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
||||||
|
with only Bittrex and Binance.
|
||||||
|
|
||||||
|
The bot was tested with the following exchanges:
|
||||||
|
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||||
|
- [Binance](https://www.binance.com/): "binance"
|
||||||
|
|
||||||
|
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
|
|
||||||
### What values for fiat_display_currency?
|
### What values for fiat_display_currency?
|
||||||
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid value are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||||
|
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||||
|
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run mode
|
||||||
We recommend starting the bot in dry-run mode to see how your bot will
|
We recommend starting the bot in dry-run mode to see how your bot will
|
||||||
@ -91,12 +120,13 @@ creating trades.
|
|||||||
|
|
||||||
### To switch your bot in Dry-run mode:
|
### To switch your bot in Dry-run mode:
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
2. Switch dry-run to true
|
2. Switch dry-run to true and specify db_url for a persistent db
|
||||||
```json
|
```json
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
|
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove your Bittrex API key (change them by fake api credentials)
|
3. Remove your Exchange API key (change them by fake api credentials)
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -117,12 +147,12 @@ you run it in production mode.
|
|||||||
### To switch your bot in production mode:
|
### To switch your bot in production mode:
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
|
|
||||||
2. Switch dry-run to false
|
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
||||||
```json
|
```json
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert your Bittrex API key (change them by fake api keys)
|
3. Insert your Exchange API key (change them by fake api keys)
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -132,9 +162,8 @@ you run it in production mode.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
If you have not your Bittrex API key yet,
|
If you have not your Bittrex API key yet,
|
||||||
[see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md).
|
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||||
|
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have configured your config.json, the next step is to
|
Now you have configured your config.json, the next step is to
|
||||||
[start your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md).
|
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||||
|
@ -27,7 +27,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
|
|||||||
#### I want to improve the bot with a new strategy
|
#### I want to improve the bot with a new strategy
|
||||||
|
|
||||||
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||||
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
||||||
|
|
||||||
#### Is there a setting to only SELL the coins being held and not
|
#### Is there a setting to only SELL the coins being held and not
|
||||||
perform anymore BUYS?
|
perform anymore BUYS?
|
||||||
|
@ -9,18 +9,17 @@ parameters with Hyperopt.
|
|||||||
- [Advanced Hyperopt notions](#advanced-notions)
|
- [Advanced Hyperopt notions](#advanced-notions)
|
||||||
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
||||||
- [Execute Hyperopt](#execute-hyperopt)
|
- [Execute Hyperopt](#execute-hyperopt)
|
||||||
- [Hyperopt with MongoDB](#hyperopt-with-mongoDB)
|
|
||||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Prepare Hyperopt
|
## Prepare Hyperopt
|
||||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||||
your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
|
|
||||||
### 1. Configure your Guards and Triggers
|
### 1. Configure your Guards and Triggers
|
||||||
There are two places you need to change in your strategy file to add a
|
There are two places you need to change in your strategy file to add a
|
||||||
new buy strategy for testing:
|
new buy strategy for testing:
|
||||||
- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||||
- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||||
|
|
||||||
There you have two different type of indicators: 1. `guards` and 2.
|
There you have two different type of indicators: 1. `guards` and 2.
|
||||||
`triggers`.
|
`triggers`.
|
||||||
@ -110,13 +109,13 @@ cannot use your config file. It is also made on purpose to allow you
|
|||||||
testing your strategy with different configurations.
|
testing your strategy with different configurations.
|
||||||
|
|
||||||
The Hyperopt configuration is located in
|
The Hyperopt configuration is located in
|
||||||
[user_data/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||||
|
|
||||||
|
|
||||||
## Advanced notions
|
## Advanced notions
|
||||||
### Understand the Guards and Triggers
|
### Understand the Guards and Triggers
|
||||||
When you need to add the new guards and triggers to be hyperopt
|
When you need to add the new guards and triggers to be hyperopt
|
||||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||||
|
|
||||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||||
|
|
||||||
@ -194,41 +193,6 @@ Legal values are:
|
|||||||
- `stoploss`: search for the best stoploss value
|
- `stoploss`: search for the best stoploss value
|
||||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
|
||||||
### Hyperopt with MongoDB
|
|
||||||
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
|
|
||||||
executing the previous command is the execution takes a long time.
|
|
||||||
To accelerate it you can use hyperopt with MongoDB.
|
|
||||||
|
|
||||||
To run hyperopt with MongoDb you will need 3 terminals.
|
|
||||||
|
|
||||||
**Terminal 1: Start MongoDB**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 scripts/start-mongodb.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 2: Start Hyperopt worker**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 scripts/start-hyperopt-worker.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 3: Start Hyperopt with MongoDB**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 ./freqtrade/main.py -c config.json hyperopt --use-mongodb
|
|
||||||
```
|
|
||||||
|
|
||||||
**Re-run an Hyperopt**
|
|
||||||
To re-run Hyperopt you have to delete the existing MongoDB table.
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
rm -rf .hyperopt/mongodb/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understand the hyperopts result
|
## Understand the hyperopts result
|
||||||
Once Hyperopt is completed you can use the result to adding new buy
|
Once Hyperopt is completed you can use the result to adding new buy
|
||||||
signal. Given following result from hyperopt:
|
signal. Given following result from hyperopt:
|
||||||
@ -312,4 +276,4 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have a perfect bot and want to control it from Telegram. Your
|
Now you have a perfect bot and want to control it from Telegram. Your
|
||||||
next step is to learn the [Telegram usage](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md).
|
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||||
|
@ -6,27 +6,27 @@ Pull-request. Do not hesitate to reach us on
|
|||||||
if you do not find the answer to your questions.
|
if you do not find the answer to your questions.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Pre-requisite](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
|
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
- [Setup your Bittrex account](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
||||||
- [Setup your Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
||||||
- [Bot Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
|
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
- [Install with Docker (all platforms)](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#docker)
|
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
|
||||||
- [Install on Linux Ubuntu](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
|
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
|
||||||
- [Install on MacOS](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
|
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
|
||||||
- [Install on Windows](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#windows)
|
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows)
|
||||||
- [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
- [Bot usage (Start your bot)](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md)
|
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
|
||||||
- [Bot commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||||
- [Backtesting commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
||||||
- [Hyperopt commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||||
- [Bot Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
- [Change your strategy](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
||||||
- [Add more Indicator](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
||||||
- [Test your strategy with Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
|
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Control the bot with telegram](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md)
|
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
||||||
- [Contribute to the project](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [How to contribute](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [Run tests & Check PEP8 compliance](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [FAQ](https://github.com/gcarq/freqtrade/blob/develop/docs/faq.md)
|
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
||||||
- [SQL cheatsheet](https://github.com/gcarq/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
This page explains how to prepare your environment for running the bot.
|
This page explains how to prepare your environment for running the bot.
|
||||||
|
|
||||||
To understand how to set up the bot please read the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
* [Table of Contents](#table-of-contents)
|
* [Table of Contents](#table-of-contents)
|
||||||
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
||||||
|
* [Manual installation](#manual-installation)
|
||||||
* [Automatic Installation - Docker](#automatic-installation---docker)
|
* [Automatic Installation - Docker](#automatic-installation---docker)
|
||||||
* [Custom Linux MacOS Installation](#custom-installation)
|
* [Custom Linux MacOS Installation](#custom-installation)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
@ -16,7 +17,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 +35,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,14 +45,39 @@ 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`.
|
||||||
|
|
||||||
|
|
||||||
|
## Manual installation - Linux/MacOS
|
||||||
|
The following steps are made for Linux/MacOS environment
|
||||||
|
|
||||||
|
**1. Clone the repo**
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
|
git checkout develop
|
||||||
|
cd freqtrade
|
||||||
|
```
|
||||||
|
**2. Create the config file**
|
||||||
|
Switch `"dry_run": true,`
|
||||||
|
```bash
|
||||||
|
cp config.json.example config.json
|
||||||
|
vi config.json
|
||||||
|
```
|
||||||
|
**3. Build your docker image and run it**
|
||||||
|
```bash
|
||||||
|
docker build -t freqtrade .
|
||||||
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Automatic Installation - Docker
|
## Automatic Installation - Docker
|
||||||
@ -63,13 +90,12 @@ 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
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1.2. (Optional) Checkout the develop branch
|
#### 1.2. (Optional) Checkout the develop branch
|
||||||
@ -90,21 +116,22 @@ cd freqtrade
|
|||||||
cp -n config.json.example config.json
|
cp -n config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
|
|
||||||
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
||||||
|
|
||||||
Production
|
Production
|
||||||
|
|
||||||
```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 +141,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 +149,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):
|
||||||
@ -132,8 +157,15 @@ You can run a one-off container that is immediately deleted upon exiting with th
|
|||||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@ -155,10 +187,11 @@ docker run -d \
|
|||||||
-v /etc/localtime:/etc/localtime:ro \
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
freqtrade
|
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you plan to use the dry run mode with the param `--dry-run-db`.
|
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||||
|
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||||
|
|
||||||
### 6. Monitor your Docker instance
|
### 6. Monitor your Docker instance
|
||||||
|
|
||||||
@ -174,6 +207,26 @@ docker start freqtrade
|
|||||||
|
|
||||||
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### 7. Backtest with docker
|
||||||
|
|
||||||
|
The following assumes that the above steps (1-4) have been completed successfully.
|
||||||
|
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||||
|
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
|
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
|
||||||
|
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
||||||
|
```
|
||||||
|
|
||||||
|
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details.
|
||||||
|
|
||||||
|
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example).
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Custom Installation
|
## Custom Installation
|
||||||
@ -183,13 +236,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
|
||||||
@ -215,35 +268,20 @@ cd ..
|
|||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. [Optional] Install MongoDB
|
#### 3. Install FreqTrade
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install mongodb-org
|
|
||||||
```
|
|
||||||
|
|
||||||
> Complete tutorial from Digital Ocean: [How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04).
|
|
||||||
|
|
||||||
#### 4. Install FreqTrade
|
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
#### 4. Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout develop
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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
|
||||||
```
|
```
|
||||||
@ -254,7 +292,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
|
||||||
@ -263,24 +300,12 @@ sudo loginctl enable-linger "$USER"
|
|||||||
brew install python3 git wget ta-lib
|
brew install python3 git wget ta-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. [Optional] Install MongoDB
|
#### 2. Install FreqTrade
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
|
|
||||||
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
|
|
||||||
mkdir -p <path_freqtrade>/env/mongodb
|
|
||||||
cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
|
|
||||||
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Install FreqTrade
|
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
Optionally checkout the develop branch:
|
||||||
@ -289,7 +314,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
|
||||||
@ -299,8 +323,7 @@ cd freqtrade
|
|||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
> *To edit the config please refer to [Bot Configuration](https://github.com/gcarq/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)
|
||||||
|
|
||||||
@ -324,27 +347,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/gcarq/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/gcarq/freqtrade/blob/develop/docs/configuration.md)...
|
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
||||||
|
@ -43,6 +43,16 @@ python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
|
|||||||
```
|
```
|
||||||
Timerange doesn't work with live data.
|
Timerange doesn't work with live data.
|
||||||
|
|
||||||
|
To plot trades stored in a database use `--db-url` argument:
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
|
||||||
|
```
|
||||||
|
|
||||||
|
To plot a test strategy the strategy should have first be backtested.
|
||||||
|
The results may then be plotted with the -s argument:
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
|
||||||
|
```
|
||||||
|
|
||||||
## Plot profit
|
## Plot profit
|
||||||
|
|
||||||
|
@ -32,9 +32,12 @@ CREATE TABLE trades (
|
|||||||
exchange VARCHAR NOT NULL,
|
exchange VARCHAR NOT NULL,
|
||||||
pair VARCHAR NOT NULL,
|
pair VARCHAR NOT NULL,
|
||||||
is_open BOOLEAN NOT NULL,
|
is_open BOOLEAN NOT NULL,
|
||||||
fee FLOAT NOT NULL,
|
fee_open FLOAT NOT NULL,
|
||||||
|
fee_close FLOAT NOT NULL,
|
||||||
open_rate FLOAT,
|
open_rate FLOAT,
|
||||||
|
open_rate_requested FLOAT,
|
||||||
close_rate FLOAT,
|
close_rate FLOAT,
|
||||||
|
close_rate_requested FLOAT,
|
||||||
close_profit FLOAT,
|
close_profit FLOAT,
|
||||||
stake_amount FLOAT NOT NULL,
|
stake_amount FLOAT NOT NULL,
|
||||||
amount FLOAT,
|
amount FLOAT,
|
||||||
@ -71,18 +74,18 @@ WHERE id=31;
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
INSERT
|
INSERT
|
||||||
INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date)
|
INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
|
||||||
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
|
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
```sql
|
```sql
|
||||||
INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
|
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fix wrong fees in the table
|
## Fix wrong fees in the table
|
||||||
If your DB was created before
|
If your DB was created before
|
||||||
[PR#200](https://github.com/gcarq/freqtrade/pull/200) was merged
|
[PR#200](https://github.com/freqtrade/freqtrade/pull/200) was merged
|
||||||
(before 12/23/17).
|
(before 12/23/17).
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -4,7 +4,7 @@ This page explains how to command your bot with Telegram.
|
|||||||
|
|
||||||
## Pre-requisite
|
## Pre-requisite
|
||||||
To control your bot with Telegram, you need first to
|
To control your bot with Telegram, you need first to
|
||||||
[set up a Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
|
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
and add your Telegram API keys into your config file.
|
and add your Telegram API keys into your config file.
|
||||||
|
|
||||||
## Telegram commands
|
## Telegram commands
|
||||||
@ -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
|
||||||
@ -42,7 +43,7 @@ Below, example of Telegram message you will receive for each command.
|
|||||||
For each open trade, the bot will send you the following message.
|
For each open trade, the bot will send you the following message.
|
||||||
|
|
||||||
> **Trade ID:** `123`
|
> **Trade ID:** `123`
|
||||||
> **Current Pair:** BTC_CVC
|
> **Current Pair:** CVC/BTC
|
||||||
> **Open Since:** `1 days ago`
|
> **Open Since:** `1 days ago`
|
||||||
> **Amount:** `26.64180098`
|
> **Amount:** `26.64180098`
|
||||||
> **Open Rate:** `0.00007489`
|
> **Open Rate:** `0.00007489`
|
||||||
@ -57,8 +58,8 @@ Return the status of all open trades in a table format.
|
|||||||
```
|
```
|
||||||
ID Pair Since Profit
|
ID Pair Since Profit
|
||||||
---- -------- ------- --------
|
---- -------- ------- --------
|
||||||
67 BTC_SC 1 d 13.33%
|
67 SC/BTC 1 d 13.33%
|
||||||
123 BTC_CVC 1 h 12.95%
|
123 CVC/BTC 1 h 12.95%
|
||||||
```
|
```
|
||||||
|
|
||||||
## /count
|
## /count
|
||||||
@ -83,7 +84,7 @@ Return a summary of your profit/loss and performance.
|
|||||||
> **First Trade opened:** `3 days ago`
|
> **First Trade opened:** `3 days ago`
|
||||||
> **Latest Trade opened:** `2 minutes ago`
|
> **Latest Trade opened:** `2 minutes ago`
|
||||||
> **Avg. Duration:** `2:33:45`
|
> **Avg. Duration:** `2:33:45`
|
||||||
> **Best Performing:** `BTC_PAY: 50.23%`
|
> **Best Performing:** `PAY/BTC: 50.23%`
|
||||||
|
|
||||||
## /forcesell <trade_id>
|
## /forcesell <trade_id>
|
||||||
|
|
||||||
@ -92,11 +93,11 @@ Return a summary of your profit/loss and performance.
|
|||||||
## /performance
|
## /performance
|
||||||
Return the performance of each crypto-currency the bot has sold.
|
Return the performance of each crypto-currency the bot has sold.
|
||||||
> Performance:
|
> Performance:
|
||||||
> 1. `BTC_RCN 57.77%`
|
> 1. `RCN/BTC 57.77%`
|
||||||
> 2. `BTC_PAY 56.91%`
|
> 2. `PAY/BTC 56.91%`
|
||||||
> 3. `BTC_VIB 47.07%`
|
> 3. `VIB/BTC 47.07%`
|
||||||
> 4. `BTC_SALT 30.24%`
|
> 4. `SALT/BTC 30.24%`
|
||||||
> 5. `BTC_STORJ 27.24%`
|
> 5. `STORJ/BTC 27.24%`
|
||||||
> ...
|
> ...
|
||||||
|
|
||||||
## /balance
|
## /balance
|
||||||
@ -129,12 +130,8 @@ Day Profit BTC Profit USD
|
|||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
### using proxy with telegram
|
### using proxy with telegram
|
||||||
in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace
|
|
||||||
```
|
```
|
||||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
$ export HTTP_PROXY="http://addr:port"
|
||||||
```
|
$ export HTTPS_PROXY="http://addr:port"
|
||||||
|
$ freqtrade
|
||||||
with
|
|
||||||
```
|
|
||||||
self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0)
|
|
||||||
```
|
```
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.16.1'
|
__version__ = '0.17.0'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(BaseException):
|
class DependencyException(BaseException):
|
||||||
@ -12,5 +12,14 @@ class DependencyException(BaseException):
|
|||||||
class OperationalException(BaseException):
|
class OperationalException(BaseException):
|
||||||
"""
|
"""
|
||||||
Requires manual intervention.
|
Requires manual intervention.
|
||||||
This happens when an exchange returns an unexpected error during runtime.
|
This happens when an exchange returns an unexpected error during runtime
|
||||||
|
or given configuration is invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TemporaryError(BaseException):
|
||||||
|
"""
|
||||||
|
Temporary network or exchange related error.
|
||||||
|
This could happen when an exchange is congested, unavailable, or the user
|
||||||
|
has networking problems. Usually resolves itself after a time.
|
||||||
"""
|
"""
|
||||||
|
15
freqtrade/__main__.py
Normal file
15
freqtrade/__main__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
__main__.py for Freqtrade
|
||||||
|
To launch Freqtrade as a module
|
||||||
|
|
||||||
|
> python -m freqtrade (with Python >= 3.6)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from freqtrade import main
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main.set_loggers()
|
||||||
|
main.main(sys.argv[1:])
|
@ -9,9 +9,10 @@ from typing import Dict, List, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade import constants
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -36,7 +37,7 @@ class Analyze(object):
|
|||||||
:param config: Bot configuration (use the one from Configuration())
|
:param config: Bot configuration (use the one from Configuration())
|
||||||
"""
|
"""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.strategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
@ -45,21 +46,23 @@ class Analyze(object):
|
|||||||
:param ticker: See exchange.get_ticker_history
|
:param ticker: See exchange.get_ticker_history
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
frame = DataFrame(ticker).rename(columns=columns)
|
frame = DataFrame(ticker, columns=cols)
|
||||||
if 'BV' in frame:
|
|
||||||
frame.drop('BV', axis=1, inplace=True)
|
|
||||||
|
|
||||||
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
frame['date'] = to_datetime(frame['date'],
|
||||||
|
unit='ms',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||||
'close': 'last',
|
'open': 'first',
|
||||||
'high': 'max',
|
'high': 'max',
|
||||||
'low': 'min',
|
'low': 'min',
|
||||||
'open': 'first',
|
'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:
|
||||||
@ -88,13 +91,20 @@ class Analyze(object):
|
|||||||
"""
|
"""
|
||||||
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
||||||
|
|
||||||
def get_ticker_interval(self) -> int:
|
def get_ticker_interval(self) -> str:
|
||||||
"""
|
"""
|
||||||
Return ticker interval to use
|
Return ticker interval to use
|
||||||
:return: Ticker interval value to use
|
:return: Ticker interval value to use
|
||||||
"""
|
"""
|
||||||
return self.strategy.ticker_interval
|
return self.strategy.ticker_interval
|
||||||
|
|
||||||
|
def get_stoploss(self) -> float:
|
||||||
|
"""
|
||||||
|
Return stoploss to use
|
||||||
|
:return: Strategy stoploss value to use
|
||||||
|
"""
|
||||||
|
return self.strategy.stoploss
|
||||||
|
|
||||||
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Parses the given ticker history and returns a populated DataFrame
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
@ -107,14 +117,14 @@ class Analyze(object):
|
|||||||
dataframe = self.populate_sell_trend(dataframe)
|
dataframe = self.populate_sell_trend(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
|
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Calculates current signal based several technical analysis indicators
|
Calculates current signal based several technical analysis indicators
|
||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
:param pair: pair in format ANT/BTC
|
||||||
:param interval: Interval to use (in min)
|
:param interval: Interval to use (in min)
|
||||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||||
"""
|
"""
|
||||||
ticker_hist = get_ticker_history(pair, interval)
|
ticker_hist = exchange.get_ticker_history(pair, interval)
|
||||||
if not ticker_hist:
|
if not ticker_hist:
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
return False, False
|
return False, False
|
||||||
@ -144,7 +154,8 @@ class Analyze(object):
|
|||||||
|
|
||||||
# Check if dataframe is out of date
|
# Check if dataframe is out of date
|
||||||
signal_date = arrow.get(latest['date'])
|
signal_date = arrow.get(latest['date'])
|
||||||
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
|
if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair,
|
pair,
|
||||||
@ -168,33 +179,45 @@ class Analyze(object):
|
|||||||
if the threshold is reached and updates the trade record.
|
if the threshold is reached and updates the trade record.
|
||||||
:return: True if trade should be sold, False otherwise
|
:return: True if trade should be sold, False otherwise
|
||||||
"""
|
"""
|
||||||
|
current_profit = trade.calc_profit_percent(rate)
|
||||||
|
if self.stop_loss_reached(current_profit=current_profit):
|
||||||
|
return True
|
||||||
|
|
||||||
|
experimental = self.config.get('experimental', {})
|
||||||
|
|
||||||
|
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||||
|
logger.debug('Buy signal still active - not selling.')
|
||||||
|
return False
|
||||||
|
|
||||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||||
if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date):
|
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
||||||
logger.debug('Required profit reached. Selling..')
|
logger.debug('Required profit reached. Selling..')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
if experimental.get('sell_profit_only', False):
|
||||||
if self.config.get('experimental', {}).get('sell_profit_only', False):
|
|
||||||
logger.debug('Checking if trade is profitable..')
|
logger.debug('Checking if trade is profitable..')
|
||||||
if trade.calc_profit(rate=rate) <= 0:
|
if trade.calc_profit(rate=rate) <= 0:
|
||||||
return False
|
return False
|
||||||
|
if sell and not buy and experimental.get('use_sell_signal', False):
|
||||||
if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False):
|
|
||||||
logger.debug('Sell signal received. Selling..')
|
logger.debug('Sell signal received. Selling..')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
def stop_loss_reached(self, current_profit: float) -> bool:
|
||||||
|
"""Based on current profit of the trade and configured stoploss, decides to sell or not"""
|
||||||
|
|
||||||
|
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
||||||
|
logger.debug('Stop loss hit.')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||||
sell
|
sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
|
||||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
|
||||||
logger.debug('Stop loss hit.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
|
@ -2,23 +2,36 @@
|
|||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from typing import List, Tuple, Optional
|
import arrow
|
||||||
|
from typing import List, Optional, NamedTuple
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRange(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple Defining timerange inputs.
|
||||||
|
[start/stop]type defines if [start/stop]ts shall be used.
|
||||||
|
if *type is none, don't use corresponding startvalue.
|
||||||
|
"""
|
||||||
|
starttype: Optional[str] = None
|
||||||
|
stoptype: Optional[str] = None
|
||||||
|
startts: int = 0
|
||||||
|
stopts: int = 0
|
||||||
|
|
||||||
|
|
||||||
class Arguments(object):
|
class Arguments(object):
|
||||||
"""
|
"""
|
||||||
Arguments Class. Manage the arguments received by the cli
|
Arguments Class. Manage the arguments received by the cli
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: List[str], description: str):
|
def __init__(self, args: List[str], description: str) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.parsed_arg = None
|
self.parsed_arg: Optional[argparse.Namespace] = None
|
||||||
self.parser = argparse.ArgumentParser(description=description)
|
self.parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
def _load_args(self) -> None:
|
def _load_args(self) -> None:
|
||||||
@ -59,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',
|
||||||
@ -71,9 +84,9 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-d', '--datadir',
|
'-d', '--datadir',
|
||||||
help='path to backtest data (default: %(default)s',
|
help='path to backtest data',
|
||||||
dest='datadir',
|
dest='datadir',
|
||||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
default=None,
|
||||||
type=str,
|
type=str,
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
@ -94,8 +107,8 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--dynamic-whitelist',
|
'--dynamic-whitelist',
|
||||||
help='dynamically generate and update whitelist \
|
help='dynamically generate and update whitelist'
|
||||||
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
' based on 24h BaseVolume (default: %(const)s)',
|
||||||
dest='dynamic_whitelist',
|
dest='dynamic_whitelist',
|
||||||
const=constants.DYNAMIC_WHITELIST,
|
const=constants.DYNAMIC_WHITELIST,
|
||||||
type=int,
|
type=int,
|
||||||
@ -103,11 +116,13 @@ class Arguments(object):
|
|||||||
nargs='?',
|
nargs='?',
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--dry-run-db',
|
'--db-url',
|
||||||
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||||
instead of memory DB. Work only if dry_run is enabled.',
|
' or in custom deployments (default: %(default)s)',
|
||||||
action='store_true',
|
dest='db_url',
|
||||||
dest='dry_run_db',
|
default=constants.DEFAULT_DB_PROD_URL,
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -123,8 +138,8 @@ class Arguments(object):
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-r', '--refresh-pairs-cached',
|
'-r', '--refresh-pairs-cached',
|
||||||
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||||
Use it if you want to run your backtesting with up-to-date data.',
|
'exchange. Use it if you want to run your backtesting with up-to-date data.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='refresh_pairs',
|
dest='refresh_pairs',
|
||||||
)
|
)
|
||||||
@ -136,15 +151,30 @@ class Arguments(object):
|
|||||||
default=None,
|
default=None,
|
||||||
dest='export',
|
dest='export',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--export-filename',
|
||||||
|
help='Save backtest results to this filename \
|
||||||
|
requires --export to be set as well\
|
||||||
|
Example --export-filename=user_data/backtest_data/backtest_today.json\
|
||||||
|
(default: %(default)s)',
|
||||||
|
type=str,
|
||||||
|
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
|
||||||
|
dest='exportfilename',
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
@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 in minutes (1, 5, 30, 60, 1440)',
|
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
||||||
dest='ticker_interval',
|
dest='ticker_interval',
|
||||||
type=int,
|
type=str,
|
||||||
metavar='INT',
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--realistic-simulation',
|
'--realistic-simulation',
|
||||||
@ -173,12 +203,6 @@ class Arguments(object):
|
|||||||
type=int,
|
type=int,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
'--use-mongodb',
|
|
||||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
|
||||||
dest='mongodb',
|
|
||||||
action='store_true',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--spaces',
|
'-s', '--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space separate list. \
|
help='Specify which parameters to hyperopt. Space separate list. \
|
||||||
@ -211,17 +235,20 @@ class Arguments(object):
|
|||||||
self.hyperopt_options(hyperopt_cmd)
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
|
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||||
"""
|
"""
|
||||||
Parse the value of the argument --timerange to determine what is the range desired
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
:return: Start and End range period
|
:return: Start and End range period
|
||||||
"""
|
"""
|
||||||
if text is None:
|
if text is None:
|
||||||
return None
|
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')),
|
||||||
|
(r'^-(\d{10})$', (None, 'date')),
|
||||||
|
(r'^(\d{10})-$', ('date', None)),
|
||||||
|
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
|
||||||
(r'^(-\d+)$', (None, 'line')),
|
(r'^(-\d+)$', (None, 'line')),
|
||||||
(r'^(\d+)-$', ('line', None)),
|
(r'^(\d+)-$', ('line', None)),
|
||||||
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||||
@ -231,23 +258,27 @@ class Arguments(object):
|
|||||||
if match: # Regex has matched
|
if match: # Regex has matched
|
||||||
rvals = match.groups()
|
rvals = match.groups()
|
||||||
index = 0
|
index = 0
|
||||||
start = None
|
start: int = 0
|
||||||
stop = None
|
stop: int = 0
|
||||||
if stype[0]:
|
if stype[0]:
|
||||||
start = rvals[index]
|
starts = rvals[index]
|
||||||
if stype[0] != 'date':
|
if stype[0] == 'date' and len(starts) == 8:
|
||||||
start = int(start)
|
start = arrow.get(starts, 'YYYYMMDD').timestamp
|
||||||
|
else:
|
||||||
|
start = int(starts)
|
||||||
index += 1
|
index += 1
|
||||||
if stype[1]:
|
if stype[1]:
|
||||||
stop = rvals[index]
|
stops = rvals[index]
|
||||||
if stype[1] != 'date':
|
if stype[1] == 'date' and len(stops) == 8:
|
||||||
stop = int(stop)
|
stop = arrow.get(stops, 'YYYYMMDD').timestamp
|
||||||
return stype, start, stop
|
else:
|
||||||
|
stop = int(stops)
|
||||||
|
return TimeRange(stype[0], stype[1], start, stop)
|
||||||
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||||
|
|
||||||
def scripts_options(self) -> None:
|
def scripts_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given arguments for plot scripts.
|
Parses given arguments for scripts.
|
||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-p', '--pair',
|
'-p', '--pair',
|
||||||
@ -255,3 +286,51 @@ class Arguments(object):
|
|||||||
dest='pair',
|
dest='pair',
|
||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testdata_dl_options(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for testdata download
|
||||||
|
"""
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--pairs-file',
|
||||||
|
help='File containing a list of pairs to download',
|
||||||
|
dest='pairs_file',
|
||||||
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--export',
|
||||||
|
help='Export files to given dir',
|
||||||
|
dest='export',
|
||||||
|
default=None,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--days',
|
||||||
|
help='Download data for number of days',
|
||||||
|
dest='days',
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--exchange',
|
||||||
|
help='Exchange name (default: %(default)s)',
|
||||||
|
dest='exchange',
|
||||||
|
type=str,
|
||||||
|
default='bittrex'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-t', '--timeframes',
|
||||||
|
help='Specify which tickers to download. Space separated list. \
|
||||||
|
Default: %(default)s',
|
||||||
|
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||||
|
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||||
|
default=['1m', '5m'],
|
||||||
|
nargs='+',
|
||||||
|
dest='timeframes',
|
||||||
|
)
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
from jsonschema import Draft4Validator, validate
|
from jsonschema import Draft4Validator, validate
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
import ccxt
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import OperationalException, constants
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -23,7 +23,7 @@ class Configuration(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, args: Namespace) -> None:
|
def __init__(self, args: Namespace) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config = None
|
self.config: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -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'] = {}
|
||||||
@ -97,19 +95,35 @@ class Configuration(object):
|
|||||||
'(not applicable with Backtesting and Hyperopt)'
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add dry_run_db if found and the bot in dry run
|
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||||
if self.args.dry_run_db and config.get('dry_run', False):
|
config.update({'db_url': self.args.db_url})
|
||||||
config.update({'dry_run_db': True})
|
logger.info('Parameter --db-url detected ...')
|
||||||
logger.info('Parameter --dry-run-db detected ...')
|
|
||||||
|
|
||||||
if config.get('dry_run_db', False):
|
if config.get('dry_run', False):
|
||||||
if config.get('dry_run', False):
|
logger.info('Dry run is enabled')
|
||||||
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||||
else:
|
# Default to in-memory db for dry_run if not specified
|
||||||
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
|
else:
|
||||||
|
if not config.get('db_url', None):
|
||||||
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
|
logger.info('Using DB: "{}"'.format(config['db_url']))
|
||||||
|
|
||||||
|
# Check if the exchange set by the user is supported
|
||||||
|
self.check_exchange(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
|
||||||
|
exchange_name = config.get('exchange', {}).get('name').lower()
|
||||||
|
default_path = os.path.join('user_data', 'data', exchange_name)
|
||||||
|
if not os.path.isdir(default_path):
|
||||||
|
os.makedirs(default_path)
|
||||||
|
logger.info(f'Created data directory: {default_path}')
|
||||||
|
return default_path
|
||||||
|
|
||||||
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load Backtesting configuration
|
Extract information for sys.argv and load Backtesting configuration
|
||||||
@ -121,7 +135,7 @@ class Configuration(object):
|
|||||||
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
||||||
config.update({'ticker_interval': self.args.ticker_interval})
|
config.update({'ticker_interval': self.args.ticker_interval})
|
||||||
logger.info('Parameter -i/--ticker-interval detected ...')
|
logger.info('Parameter -i/--ticker-interval detected ...')
|
||||||
logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
|
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
|
||||||
|
|
||||||
# If -l/--live is used we add it to the configuration
|
# If -l/--live is used we add it to the configuration
|
||||||
if 'live' in self.args and self.args.live:
|
if 'live' in self.args and self.args.live:
|
||||||
@ -142,7 +156,9 @@ class Configuration(object):
|
|||||||
# If --datadir is used we add it to the configuration
|
# If --datadir is used we add it to the configuration
|
||||||
if 'datadir' in self.args and self.args.datadir:
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
config.update({'datadir': self.args.datadir})
|
config.update({'datadir': self.args.datadir})
|
||||||
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
|
else:
|
||||||
|
config.update({'datadir': self._create_default_datadir(config)})
|
||||||
|
logger.info('Using data folder: %s ...', config.get('datadir'))
|
||||||
|
|
||||||
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
||||||
@ -154,6 +170,11 @@ class Configuration(object):
|
|||||||
config.update({'export': self.args.export})
|
config.update({'export': self.args.export})
|
||||||
logger.info('Parameter --export detected: %s ...', self.args.export)
|
logger.info('Parameter --export detected: %s ...', self.args.export)
|
||||||
|
|
||||||
|
# If --export-filename is used we add it to the configuration
|
||||||
|
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
|
||||||
|
config.update({'exportfilename': self.args.exportfilename})
|
||||||
|
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@ -167,11 +188,6 @@ class Configuration(object):
|
|||||||
logger.info('Parameter --epochs detected ...')
|
logger.info('Parameter --epochs detected ...')
|
||||||
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
||||||
|
|
||||||
# If --mongodb is used we add it to the configuration
|
|
||||||
if 'mongodb' in self.args and self.args.mongodb:
|
|
||||||
config.update({'mongodb': self.args.mongodb})
|
|
||||||
logger.info('Parameter --use-mongodb detected ...')
|
|
||||||
|
|
||||||
# If --spaces is used we add it to the configuration
|
# If --spaces is used we add it to the configuration
|
||||||
if 'spaces' in self.args and self.args.spaces:
|
if 'spaces' in self.args and self.args.spaces:
|
||||||
config.update({'spaces': self.args.spaces})
|
config.update({'spaces': self.args.spaces})
|
||||||
@ -189,7 +205,7 @@ class Configuration(object):
|
|||||||
validate(conf, constants.CONF_SCHEMA)
|
validate(conf, constants.CONF_SCHEMA)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as exception:
|
except ValidationError as exception:
|
||||||
logger.fatal(
|
logger.critical(
|
||||||
'Invalid configuration. See config.json.example. Reason: %s',
|
'Invalid configuration. See config.json.example. Reason: %s',
|
||||||
exception
|
exception
|
||||||
)
|
)
|
||||||
@ -206,3 +222,22 @@ class Configuration(object):
|
|||||||
self.config = self.load_config()
|
self.config = self.load_config()
|
||||||
|
|
||||||
return self.config
|
return self.config
|
||||||
|
|
||||||
|
def check_exchange(self, config: Dict[str, Any]) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the exchange name in the config file is supported by Freqtrade
|
||||||
|
:return: True or raised an exception if the exchange if not supported
|
||||||
|
"""
|
||||||
|
exchange = config.get('exchange', {}).get('name').lower()
|
||||||
|
if exchange not in ccxt.exchanges:
|
||||||
|
|
||||||
|
exception_msg = f'Exchange "{exchange}" not supported.\n' \
|
||||||
|
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
|
||||||
|
|
||||||
|
logger.critical(exception_msg)
|
||||||
|
raise OperationalException(
|
||||||
|
exception_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug('Exchange "%s" supported', exchange)
|
||||||
|
return True
|
||||||
|
@ -9,23 +9,49 @@ TICKER_INTERVAL = 5 # min
|
|||||||
HYPEROPT_EPOCH = 100 # epochs
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
RETRY_TIMEOUT = 30 # sec
|
RETRY_TIMEOUT = 30 # sec
|
||||||
DEFAULT_STRATEGY = 'DefaultStrategy'
|
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||||
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
|
|
||||||
|
|
||||||
|
TICKER_INTERVAL_MINUTES = {
|
||||||
|
'1m': 1,
|
||||||
|
'3m': 3,
|
||||||
|
'5m': 5,
|
||||||
|
'15m': 15,
|
||||||
|
'30m': 30,
|
||||||
|
'1h': 60,
|
||||||
|
'2h': 120,
|
||||||
|
'4h': 240,
|
||||||
|
'6h': 360,
|
||||||
|
'8h': 480,
|
||||||
|
'12h': 720,
|
||||||
|
'1d': 1440,
|
||||||
|
'3d': 4320,
|
||||||
|
'1w': 10080,
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORTED_FIAT = [
|
||||||
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||||
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
|
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||||
|
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
|
]
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
||||||
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
|
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
'stake_amount': {
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
"type": ["number", "string"],
|
||||||
'CLP', 'CNY', 'CZK', 'DKK',
|
"minimum": 0.0005,
|
||||||
'EUR', 'GBP', 'HKD', 'HUF',
|
"pattern": UNLIMITED_STAKE_AMOUNT
|
||||||
'IDR', 'ILS', 'INR', 'JPY',
|
},
|
||||||
'KRW', 'MXN', 'MYR', 'NOK',
|
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||||
'NZD', 'PHP', 'PKR', 'PLN',
|
|
||||||
'RUB', 'SEK', 'SGD', 'THB',
|
|
||||||
'TRY', 'TWD', 'ZAR', 'USD']},
|
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -53,7 +79,8 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
'use_sell_signal': {'type': 'boolean'},
|
||||||
'sell_profit_only': {'type': 'boolean'}
|
'sell_profit_only': {'type': 'boolean'},
|
||||||
|
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
@ -65,6 +92,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
},
|
},
|
||||||
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'internals': {
|
'internals': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -85,7 +113,7 @@ CONF_SCHEMA = {
|
|||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {
|
'items': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
},
|
},
|
||||||
@ -93,7 +121,7 @@ CONF_SCHEMA = {
|
|||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {
|
'items': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
|
@ -1,185 +1,417 @@
|
|||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Cryptocurrency Exchanges support """
|
""" Cryptocurrency Exchanges support """
|
||||||
import enum
|
|
||||||
import logging
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import ccxt
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
|
||||||
from cachetools import cached, TTLCache
|
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
from freqtrade.exchange.interface import Exchange
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Current selected exchange
|
API_RETRY_COUNT = 4
|
||||||
_API: Exchange = None
|
|
||||||
_CONF: dict = {}
|
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
|
||||||
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Exchanges(enum.Enum):
|
# Urls to exchange markets, insert quote and base with .format()
|
||||||
"""
|
_EXCHANGE_URLS = {
|
||||||
Maps supported exchange names to correspondent classes.
|
ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}',
|
||||||
"""
|
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}'
|
||||||
BITTREX = Bittrex
|
}
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
def retrier(f):
|
||||||
"""
|
def wrapper(*args, **kwargs):
|
||||||
Initializes this module with the given config,
|
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||||
it does basic validation whether the specified
|
try:
|
||||||
exchange and pairs are valid.
|
return f(*args, **kwargs)
|
||||||
:param config: config to use
|
except (TemporaryError, DependencyException) as ex:
|
||||||
:return: None
|
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||||
"""
|
if count > 0:
|
||||||
global _CONF, _API
|
count -= 1
|
||||||
|
kwargs.update({'count': count})
|
||||||
_CONF.update(config)
|
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||||
|
return wrapper(*args, **kwargs)
|
||||||
if config['dry_run']:
|
else:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||||
|
raise ex
|
||||||
exchange_config = config['exchange']
|
return wrapper
|
||||||
|
|
||||||
# Find matching class for the given exchange name
|
|
||||||
name = exchange_config['name']
|
|
||||||
try:
|
|
||||||
exchange_class = Exchanges[name.upper()].value
|
|
||||||
except KeyError:
|
|
||||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
|
||||||
|
|
||||||
_API = exchange_class(exchange_config)
|
|
||||||
|
|
||||||
# Check if all pairs are available
|
|
||||||
validate_pairs(config['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
def validate_pairs(pairs: List[str]) -> None:
|
class Exchange(object):
|
||||||
"""
|
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
|
||||||
Raises OperationalException if one pair is not available.
|
|
||||||
:param pairs: list of pairs
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
markets = _API.get_markets()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
|
||||||
return
|
|
||||||
|
|
||||||
stake_cur = _CONF['stake_currency']
|
# Current selected exchange
|
||||||
for pair in pairs:
|
_api: ccxt.Exchange = None
|
||||||
if not pair.startswith(stake_cur):
|
_conf: Dict = {}
|
||||||
|
_cached_ticker: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Holds all open sell orders for dry_run
|
||||||
|
_dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes this module with the given config,
|
||||||
|
it does basic validation whether the specified
|
||||||
|
exchange and pairs are valid.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self._conf.update(config)
|
||||||
|
|
||||||
|
if config['dry_run']:
|
||||||
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
|
exchange_config = config['exchange']
|
||||||
|
self._api = self._init_ccxt(exchange_config)
|
||||||
|
|
||||||
|
logger.info('Using Exchange "%s"', self.name)
|
||||||
|
|
||||||
|
# Check if all pairs are available
|
||||||
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
||||||
|
"""
|
||||||
|
Initialize ccxt with given config and return valid
|
||||||
|
ccxt instance.
|
||||||
|
"""
|
||||||
|
# Find matching class for the given exchange name
|
||||||
|
name = exchange_config['name']
|
||||||
|
|
||||||
|
if name not in ccxt.exchanges:
|
||||||
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
try:
|
||||||
|
api = getattr(ccxt, name.lower())({
|
||||||
|
'apiKey': exchange_config.get('key'),
|
||||||
|
'secret': exchange_config.get('secret'),
|
||||||
|
'password': exchange_config.get('password'),
|
||||||
|
'uid': exchange_config.get('uid', ''),
|
||||||
|
'enableRateLimit': True,
|
||||||
|
})
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
|
||||||
|
return api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""exchange Name (from ccxt)"""
|
||||||
|
return self._api.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> str:
|
||||||
|
"""exchange ccxt id"""
|
||||||
|
return self._api.id
|
||||||
|
|
||||||
|
def validate_pairs(self, pairs: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
|
Raises OperationalException if one pair is not available.
|
||||||
|
:param pairs: list of pairs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
markets = self._api.load_markets()
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
||||||
|
return
|
||||||
|
|
||||||
|
stake_cur = self._conf['stake_currency']
|
||||||
|
for pair in pairs:
|
||||||
|
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
|
||||||
|
# TODO: add a support for having coins in BTC/USDT format
|
||||||
|
if not pair.endswith(stake_cur):
|
||||||
|
raise OperationalException(
|
||||||
|
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||||
|
if pair not in markets:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Pair {pair} is not available at {self.name}')
|
||||||
|
|
||||||
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if exchange implements a specific API endpoint.
|
||||||
|
Wrapper around ccxt 'has' attribute
|
||||||
|
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
return endpoint in self._api.has and self._api.has[endpoint]
|
||||||
|
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
|
self._dry_run_open_orders[order_id] = {
|
||||||
|
'pair': pair,
|
||||||
|
'price': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'remaining': 0.0,
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'status': 'closed',
|
||||||
|
'fee': None
|
||||||
|
}
|
||||||
|
return {'id': order_id}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._api.create_limit_buy_order(pair, amount, rate)
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||||
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Could not create limit buy order on market {pair}.'
|
||||||
|
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
def sell(self, pair: str, rate: float, amount: float) -> Dict:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||||
|
self._dry_run_open_orders[order_id] = {
|
||||||
|
'pair': pair,
|
||||||
|
'price': rate,
|
||||||
|
'amount': amount,
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'sell',
|
||||||
|
'remaining': 0.0,
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'status': 'closed'
|
||||||
|
}
|
||||||
|
return {'id': order_id}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._api.create_limit_sell_order(pair, amount, rate)
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||||
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Could not create limit sell order on market {pair}.'
|
||||||
|
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_balance(self, currency: str) -> float:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
# ccxt exception is already handled by get_balances
|
||||||
|
balances = self.get_balances()
|
||||||
|
balance = balances.get(currency)
|
||||||
|
if balance is None:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get {currency} balance due to malformed exchange response: {balances}')
|
||||||
|
return balance['free']
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_balances(self) -> dict:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
balances = self._api.fetch_balance()
|
||||||
|
# Remove additional info from ccxt results
|
||||||
|
balances.pop("info", None)
|
||||||
|
balances.pop("free", None)
|
||||||
|
balances.pop("total", None)
|
||||||
|
balances.pop("used", None)
|
||||||
|
|
||||||
|
return balances
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_tickers(self) -> Dict:
|
||||||
|
try:
|
||||||
|
return self._api.fetch_tickers()
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
|
f'Exchange {self._api.name} does not support fetching tickers in batch.'
|
||||||
)
|
f'Message: {e}')
|
||||||
if pair not in markets:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
||||||
|
if refresh or pair not in self._cached_ticker.keys():
|
||||||
|
try:
|
||||||
|
data = self._api.fetch_ticker(pair)
|
||||||
|
try:
|
||||||
|
self._cached_ticker[pair] = {
|
||||||
|
'bid': float(data['bid']),
|
||||||
|
'ask': float(data['ask']),
|
||||||
|
}
|
||||||
|
except KeyError:
|
||||||
|
logger.debug("Could not cache ticker data for %s", pair)
|
||||||
|
return data
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
else:
|
||||||
|
logger.info("returning cached ticker-data for %s", pair)
|
||||||
|
return self._cached_ticker[pair]
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_ticker_history(self, pair: str, tick_interval: str,
|
||||||
|
since_ms: Optional[int] = None) -> List[Dict]:
|
||||||
|
try:
|
||||||
|
# last item should be in the time interval [now - tick_interval, now]
|
||||||
|
till_time_ms = arrow.utcnow().shift(
|
||||||
|
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||||
|
).timestamp * 1000
|
||||||
|
# it looks as if some exchanges return cached data
|
||||||
|
# and they update it one in several minute, so 10 mins interval
|
||||||
|
# is necessary to skeep downloading of an empty array when all
|
||||||
|
# chached data was already downloaded
|
||||||
|
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
|
||||||
|
|
||||||
|
data: List[Dict[Any, Any]] = []
|
||||||
|
while not since_ms or since_ms < till_time_ms:
|
||||||
|
data_part = self._api.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
|
||||||
|
|
||||||
|
# Because some exchange sort Tickers ASC and other DESC.
|
||||||
|
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||||
|
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||||
|
data_part = sorted(data_part, key=lambda x: x[0])
|
||||||
|
|
||||||
|
if not data_part:
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.debug('Downloaded data for %s time range [%s, %s]',
|
||||||
|
pair,
|
||||||
|
arrow.get(data_part[0][0] / 1000).format(),
|
||||||
|
arrow.get(data_part[-1][0] / 1000).format())
|
||||||
|
|
||||||
|
data.extend(data_part)
|
||||||
|
since_ms = data[-1][0] + 1
|
||||||
|
|
||||||
|
return data
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
return
|
||||||
|
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
try:
|
||||||
if _CONF['dry_run']:
|
return self._api.cancel_order(order_id, pair)
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
except ccxt.InvalidOrder as e:
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
raise DependencyException(
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
f'Could not cancel order. Message: {e}')
|
||||||
'pair': pair,
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
'rate': rate,
|
raise TemporaryError(
|
||||||
'amount': amount,
|
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
|
||||||
'type': 'LIMIT_BUY',
|
except ccxt.BaseError as e:
|
||||||
'remaining': 0.0,
|
raise OperationalException(e)
|
||||||
'opened': arrow.utcnow().datetime,
|
|
||||||
'closed': arrow.utcnow().datetime,
|
|
||||||
}
|
|
||||||
return order_id
|
|
||||||
|
|
||||||
return _API.buy(pair, rate, amount)
|
@retrier
|
||||||
|
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
order = self._dry_run_open_orders[order_id]
|
||||||
|
order.update({
|
||||||
|
'id': order_id
|
||||||
|
})
|
||||||
|
return order
|
||||||
|
try:
|
||||||
|
return self._api.fetch_order(order_id, pair)
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Could not get order. Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
return []
|
||||||
|
if not self.exchange_has('fetchMyTrades'):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
my_trades = self._api.fetch_my_trades(pair, since.timestamp())
|
||||||
|
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||||
|
|
||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
return matched_trades
|
||||||
if _CONF['dry_run']:
|
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
|
||||||
'pair': pair,
|
|
||||||
'rate': rate,
|
|
||||||
'amount': amount,
|
|
||||||
'type': 'LIMIT_SELL',
|
|
||||||
'remaining': 0.0,
|
|
||||||
'opened': arrow.utcnow().datetime,
|
|
||||||
'closed': arrow.utcnow().datetime,
|
|
||||||
}
|
|
||||||
return order_id
|
|
||||||
|
|
||||||
return _API.sell(pair, rate, amount)
|
except ccxt.NetworkError as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get trades due to networking error. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
|
try:
|
||||||
|
url_base = self._api.urls.get('www')
|
||||||
|
base, quote = pair.split('/')
|
||||||
|
|
||||||
def get_balance(currency: str) -> float:
|
return url_base + _EXCHANGE_URLS[self._api.id].format(base=base, quote=quote)
|
||||||
if _CONF['dry_run']:
|
except KeyError:
|
||||||
return 999.9
|
logger.warning('Could not get exchange url for %s', self.name)
|
||||||
|
return ""
|
||||||
|
|
||||||
return _API.get_balance(currency)
|
@retrier
|
||||||
|
def get_markets(self) -> List[dict]:
|
||||||
|
try:
|
||||||
|
return self._api.fetch_markets()
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
|
||||||
|
price=1, taker_or_maker='maker') -> float:
|
||||||
|
try:
|
||||||
|
# validate that markets are loaded before trying to get fee
|
||||||
|
if self._api.markets is None or len(self._api.markets) == 0:
|
||||||
|
self._api.load_markets()
|
||||||
|
|
||||||
def get_balances():
|
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
|
||||||
if _CONF['dry_run']:
|
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||||
return []
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
return _API.get_balances()
|
def get_amount_lots(self, pair: str, amount: float) -> float:
|
||||||
|
"""
|
||||||
|
get buyable amount rounding, ..
|
||||||
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
"""
|
||||||
return _API.get_ticker(pair, refresh)
|
# validate that markets are loaded before trying to get fee
|
||||||
|
if not self._api.markets:
|
||||||
|
self._api.load_markets()
|
||||||
@cached(TTLCache(maxsize=100, ttl=30))
|
return self._api.amount_to_lots(pair, amount)
|
||||||
def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
|
|
||||||
return _API.get_ticker_history(pair, tick_interval)
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_id: str) -> None:
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return
|
|
||||||
|
|
||||||
return _API.cancel_order(order_id)
|
|
||||||
|
|
||||||
|
|
||||||
def get_order(order_id: str) -> Dict:
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
|
||||||
order.update({
|
|
||||||
'id': order_id
|
|
||||||
})
|
|
||||||
return order
|
|
||||||
|
|
||||||
return _API.get_order(order_id)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pair_detail_url(pair: str) -> str:
|
|
||||||
return _API.get_pair_detail_url(pair)
|
|
||||||
|
|
||||||
|
|
||||||
def get_markets() -> List[str]:
|
|
||||||
return _API.get_markets()
|
|
||||||
|
|
||||||
|
|
||||||
def get_market_summaries() -> List[Dict]:
|
|
||||||
return _API.get_market_summaries()
|
|
||||||
|
|
||||||
|
|
||||||
def get_name() -> str:
|
|
||||||
return _API.name
|
|
||||||
|
|
||||||
|
|
||||||
def get_fee() -> float:
|
|
||||||
return _API.fee
|
|
||||||
|
|
||||||
|
|
||||||
def get_wallet_health() -> List[Dict]:
|
|
||||||
return _API.get_wallet_health()
|
|
||||||
|
@ -1,211 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from bittrex.bittrex import API_V1_1, API_V2_0
|
|
||||||
from bittrex.bittrex import Bittrex as _Bittrex
|
|
||||||
from requests.exceptions import ContentDecodingError
|
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
|
||||||
from freqtrade.exchange.interface import Exchange
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_API: _Bittrex = None
|
|
||||||
_API_V2: _Bittrex = None
|
|
||||||
_EXCHANGE_CONF: dict = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
|
||||||
"""
|
|
||||||
Bittrex API wrapper.
|
|
||||||
"""
|
|
||||||
# Base URL and API endpoints
|
|
||||||
BASE_URL: str = 'https://www.bittrex.com'
|
|
||||||
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
|
||||||
global _API, _API_V2, _EXCHANGE_CONF
|
|
||||||
|
|
||||||
_EXCHANGE_CONF.update(config)
|
|
||||||
_API = _Bittrex(
|
|
||||||
api_key=_EXCHANGE_CONF['key'],
|
|
||||||
api_secret=_EXCHANGE_CONF['secret'],
|
|
||||||
calls_per_second=1,
|
|
||||||
api_version=API_V1_1,
|
|
||||||
)
|
|
||||||
_API_V2 = _Bittrex(
|
|
||||||
api_key=_EXCHANGE_CONF['key'],
|
|
||||||
api_secret=_EXCHANGE_CONF['secret'],
|
|
||||||
calls_per_second=1,
|
|
||||||
api_version=API_V2_0,
|
|
||||||
)
|
|
||||||
self.cached_ticker = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_response(response) -> None:
|
|
||||||
"""
|
|
||||||
Validates the given bittrex response
|
|
||||||
and raises a ContentDecodingError if a non-fatal issue happened.
|
|
||||||
"""
|
|
||||||
temp_error_messages = [
|
|
||||||
'NO_API_RESPONSE',
|
|
||||||
'MIN_TRADE_REQUIREMENT_NOT_MET',
|
|
||||||
]
|
|
||||||
if response['message'] in temp_error_messages:
|
|
||||||
raise ContentDecodingError(response['message'])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fee(self) -> float:
|
|
||||||
# 0.25 %: See https://bittrex.com/fees
|
|
||||||
return 0.0025
|
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
pair=pair,
|
|
||||||
rate=rate,
|
|
||||||
amount=amount))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
|
||||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
pair=pair,
|
|
||||||
rate=rate,
|
|
||||||
amount=amount))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
def get_balance(self, currency: str) -> float:
|
|
||||||
data = _API.get_balance(currency)
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({currency})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
currency=currency))
|
|
||||||
return float(data['result']['Balance'] or 0.0)
|
|
||||||
|
|
||||||
def get_balances(self):
|
|
||||||
data = _API.get_balances()
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message}'.format(message=data['message']))
|
|
||||||
return data['result']
|
|
||||||
|
|
||||||
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
|
||||||
if refresh or pair not in self.cached_ticker.keys():
|
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({pair})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
pair=pair))
|
|
||||||
keys = ['Bid', 'Ask', 'Last']
|
|
||||||
if not data.get('result') or\
|
|
||||||
not all(key in data.get('result', {}) for key in keys) or\
|
|
||||||
not all(data.get('result', {})[key] is not None for key in keys):
|
|
||||||
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
|
|
||||||
pair=pair))
|
|
||||||
# Update the pair
|
|
||||||
self.cached_ticker[pair] = {
|
|
||||||
'bid': float(data['result']['Bid']),
|
|
||||||
'ask': float(data['result']['Ask']),
|
|
||||||
'last': float(data['result']['Last']),
|
|
||||||
}
|
|
||||||
return self.cached_ticker[pair]
|
|
||||||
|
|
||||||
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
|
||||||
if tick_interval == 1:
|
|
||||||
interval = 'oneMin'
|
|
||||||
elif tick_interval == 5:
|
|
||||||
interval = 'fiveMin'
|
|
||||||
elif tick_interval == 30:
|
|
||||||
interval = 'thirtyMin'
|
|
||||||
elif tick_interval == 60:
|
|
||||||
interval = 'hour'
|
|
||||||
elif tick_interval == 1440:
|
|
||||||
interval = 'Day'
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown tick_interval: {}'.format(tick_interval))
|
|
||||||
|
|
||||||
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
|
||||||
|
|
||||||
# These sanity check are necessary because bittrex cannot keep their API stable.
|
|
||||||
if not data.get('result'):
|
|
||||||
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
|
|
||||||
pair=pair))
|
|
||||||
|
|
||||||
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
|
||||||
for tick in data['result']:
|
|
||||||
if prop not in tick.keys():
|
|
||||||
raise ContentDecodingError('Required property {} not present '
|
|
||||||
'in response params=({})'.format(prop, pair))
|
|
||||||
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({pair})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
pair=pair))
|
|
||||||
|
|
||||||
return data['result']
|
|
||||||
|
|
||||||
def get_order(self, order_id: str) -> Dict:
|
|
||||||
data = _API.get_order(order_id)
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({order_id})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
order_id=order_id))
|
|
||||||
data = data['result']
|
|
||||||
return {
|
|
||||||
'id': data['OrderUuid'],
|
|
||||||
'type': data['Type'],
|
|
||||||
'pair': data['Exchange'].replace('-', '_'),
|
|
||||||
'opened': data['Opened'],
|
|
||||||
'rate': data['PricePerUnit'],
|
|
||||||
'amount': data['Quantity'],
|
|
||||||
'remaining': data['QuantityRemaining'],
|
|
||||||
'closed': data['Closed'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def cancel_order(self, order_id: str) -> None:
|
|
||||||
data = _API.cancel(order_id)
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException('{message} params=({order_id})'.format(
|
|
||||||
message=data['message'],
|
|
||||||
order_id=order_id))
|
|
||||||
|
|
||||||
def get_pair_detail_url(self, pair: str) -> str:
|
|
||||||
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
|
||||||
|
|
||||||
def get_markets(self) -> List[str]:
|
|
||||||
data = _API.get_markets()
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException(data['message'])
|
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
|
||||||
|
|
||||||
def get_market_summaries(self) -> List[Dict]:
|
|
||||||
data = _API.get_market_summaries()
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException(data['message'])
|
|
||||||
return data['result']
|
|
||||||
|
|
||||||
def get_wallet_health(self) -> List[Dict]:
|
|
||||||
data = _API_V2.get_wallet_health()
|
|
||||||
if not data['success']:
|
|
||||||
Bittrex._validate_response(data)
|
|
||||||
raise OperationalException(data['message'])
|
|
||||||
return [{
|
|
||||||
'Currency': entry['Health']['Currency'],
|
|
||||||
'IsActive': entry['Health']['IsActive'],
|
|
||||||
'LastChecked': entry['Health']['LastChecked'],
|
|
||||||
'Notice': entry['Currency'].get('Notice'),
|
|
||||||
} for entry in data['result']]
|
|
@ -1,172 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange(ABC):
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""
|
|
||||||
Name of the exchange.
|
|
||||||
:return: str representation of the class name
|
|
||||||
"""
|
|
||||||
return self.__class__.__name__
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fee(self) -> float:
|
|
||||||
"""
|
|
||||||
Fee for placing an order
|
|
||||||
:return: percentage in float
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit buy order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to purchase
|
|
||||||
:return: order_id of the placed buy order
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit sell order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to sell
|
|
||||||
:return: order_id of the placed sell order
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_balance(self, currency: str) -> float:
|
|
||||||
"""
|
|
||||||
Gets account balance.
|
|
||||||
:param currency: Currency as str, format: BTC
|
|
||||||
:return: float
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_balances(self) -> List[dict]:
|
|
||||||
"""
|
|
||||||
Gets account balances across currencies
|
|
||||||
:return: List of dicts, format: [
|
|
||||||
{
|
|
||||||
'Currency': str,
|
|
||||||
'Balance': float,
|
|
||||||
'Available': float,
|
|
||||||
'Pending': float,
|
|
||||||
}
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
|
||||||
"""
|
|
||||||
Gets ticker for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:param refresh: Shall we query a new value or a cached value is enough
|
|
||||||
:return: dict, format: {
|
|
||||||
'bid': float,
|
|
||||||
'ask': float,
|
|
||||||
'last': float
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Gets ticker history for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:param tick_interval: ticker interval in minutes
|
|
||||||
:return: list, format: [
|
|
||||||
{
|
|
||||||
'O': float, (Open)
|
|
||||||
'H': float, (High)
|
|
||||||
'L': float, (Low)
|
|
||||||
'C': float, (Close)
|
|
||||||
'V': float, (Volume)
|
|
||||||
'T': datetime, (Time)
|
|
||||||
'BV': float, (Base Volume)
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_order(self, order_id: str) -> Dict:
|
|
||||||
"""
|
|
||||||
Get order details for the given order_id.
|
|
||||||
:param order_id: ID as str
|
|
||||||
:return: dict, format: {
|
|
||||||
'id': str,
|
|
||||||
'type': str,
|
|
||||||
'pair': str,
|
|
||||||
'opened': str ISO 8601 datetime,
|
|
||||||
'closed': str ISO 8601 datetime,
|
|
||||||
'rate': float,
|
|
||||||
'amount': float,
|
|
||||||
'remaining': int
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def cancel_order(self, order_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Cancels order for given order_id.
|
|
||||||
:param order_id: ID as str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_pair_detail_url(self, pair: str) -> str:
|
|
||||||
"""
|
|
||||||
Returns the market detail url for the given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: URL as str
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_markets(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns all available markets.
|
|
||||||
:return: List of all available pairs
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_market_summaries(self) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Returns a 24h market summary for all available markets
|
|
||||||
:return: list, format: [
|
|
||||||
{
|
|
||||||
'MarketName': str,
|
|
||||||
'High': float,
|
|
||||||
'Low': float,
|
|
||||||
'Volume': float,
|
|
||||||
'Last': float,
|
|
||||||
'TimeStamp': datetime,
|
|
||||||
'BaseVolume': float,
|
|
||||||
'Bid': float,
|
|
||||||
'Ask': float,
|
|
||||||
'OpenBuyOrders': int,
|
|
||||||
'OpenSellOrders': int,
|
|
||||||
'PrevDay': float,
|
|
||||||
'Created': datetime
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_wallet_health(self) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Returns a list of all wallet health information
|
|
||||||
:return: list, format: [
|
|
||||||
{
|
|
||||||
'Currency': str,
|
|
||||||
'IsActive': bool,
|
|
||||||
'LastChecked': str,
|
|
||||||
'Notice': str
|
|
||||||
},
|
|
||||||
...
|
|
||||||
"""
|
|
@ -5,8 +5,11 @@ e.g BTC to USD
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
from coinmarketcap import Market
|
from coinmarketcap import Market
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
from freqtrade.constants import SUPPORTED_FIAT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,7 +35,7 @@ class CryptoFiat(object):
|
|||||||
self.price = 0.0
|
self.price = 0.0
|
||||||
|
|
||||||
# Private attributes
|
# Private attributes
|
||||||
self._expiration = 0
|
self._expiration = 0.0
|
||||||
|
|
||||||
self.crypto_symbol = crypto_symbol.upper()
|
self.crypto_symbol = crypto_symbol.upper()
|
||||||
self.fiat_symbol = fiat_symbol.upper()
|
self.fiat_symbol = fiat_symbol.upper()
|
||||||
@ -63,21 +66,9 @@ class CryptoToFiatConverter(object):
|
|||||||
This object is also a Singleton
|
This object is also a Singleton
|
||||||
"""
|
"""
|
||||||
__instance = None
|
__instance = None
|
||||||
_coinmarketcap = None
|
_coinmarketcap: Market = None
|
||||||
|
|
||||||
# Constants
|
_cryptomap: Dict = {}
|
||||||
SUPPORTED_FIAT = [
|
|
||||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
|
||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
|
||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
|
||||||
]
|
|
||||||
|
|
||||||
CRYPTOMAP = {
|
|
||||||
'BTC': 'bitcoin',
|
|
||||||
'ETH': 'ethereum',
|
|
||||||
'USDT': 'thether'
|
|
||||||
}
|
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if CryptoToFiatConverter.__instance is None:
|
if CryptoToFiatConverter.__instance is None:
|
||||||
@ -89,7 +80,19 @@ class CryptoToFiatConverter(object):
|
|||||||
return CryptoToFiatConverter.__instance
|
return CryptoToFiatConverter.__instance
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._pairs = []
|
self._pairs: List[CryptoFiat] = []
|
||||||
|
self._load_cryptomap()
|
||||||
|
|
||||||
|
def _load_cryptomap(self) -> None:
|
||||||
|
try:
|
||||||
|
coinlistings = self._coinmarketcap.listings()
|
||||||
|
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
||||||
|
coinlistings["data"]))
|
||||||
|
except (ValueError, RequestException) as exception:
|
||||||
|
logger.error(
|
||||||
|
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
|
||||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
"""
|
"""
|
||||||
@ -99,6 +102,8 @@ class CryptoToFiatConverter(object):
|
|||||||
:param fiat_symbol: fiat to convert to
|
:param fiat_symbol: fiat to convert to
|
||||||
:return: float, value in fiat of the crypto-currency amount
|
:return: float, value in fiat of the crypto-currency amount
|
||||||
"""
|
"""
|
||||||
|
if crypto_symbol == fiat_symbol:
|
||||||
|
return crypto_amount
|
||||||
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
|
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
|
||||||
return float(crypto_amount) * float(price)
|
return float(crypto_amount) * float(price)
|
||||||
|
|
||||||
@ -114,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:
|
||||||
@ -166,7 +171,7 @@ class CryptoToFiatConverter(object):
|
|||||||
|
|
||||||
fiat = fiat.upper()
|
fiat = fiat.upper()
|
||||||
|
|
||||||
return fiat in self.SUPPORTED_FIAT
|
return fiat in SUPPORTED_FIAT
|
||||||
|
|
||||||
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
"""
|
"""
|
||||||
@ -177,17 +182,24 @@ 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
|
||||||
|
if crypto_symbol == fiat_symbol:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
if crypto_symbol not in self._cryptomap:
|
||||||
|
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
|
||||||
|
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
if crypto_symbol not in self.CRYPTOMAP:
|
|
||||||
raise ValueError(
|
|
||||||
'The crypto symbol {} is not supported.'.format(crypto_symbol))
|
|
||||||
try:
|
try:
|
||||||
return float(
|
return float(
|
||||||
self._coinmarketcap.ticker(
|
self._coinmarketcap.ticker(
|
||||||
currency=self.CRYPTOMAP[crypto_symbol],
|
currency=self._cryptomap[crypto_symbol],
|
||||||
convert=fiat_symbol
|
convert=fiat_symbol
|
||||||
)[0]['price_' + fiat_symbol.lower()]
|
)['data']['quotes'][fiat_symbol.upper()]['price']
|
||||||
)
|
)
|
||||||
except BaseException:
|
except BaseException as exception:
|
||||||
|
logger.error("Error in _find_price: %s", exception)
|
||||||
return 0.0
|
return 0.0
|
||||||
|
@ -3,7 +3,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@ -12,19 +11,19 @@ from typing import Dict, List, Optional, Any, Callable
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
from freqtrade import (
|
from freqtrade import (
|
||||||
DependencyException, OperationalException, exchange, persistence, __version__
|
DependencyException, OperationalException, TemporaryError, persistence, __version__,
|
||||||
)
|
)
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc_manager import RPCManager
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -34,12 +33,11 @@ class FreqtradeBot(object):
|
|||||||
This is from here the bot start its logic.
|
This is from here the bot start its logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
|
def __init__(self, config: Dict[str, Any])-> None:
|
||||||
"""
|
"""
|
||||||
Init all variables and object the bot need to work
|
Init all variables and object the bot need to work
|
||||||
:param config: configuration dict, you can use the Configuration.get_config()
|
:param config: configuration dict, you can use the Configuration.get_config()
|
||||||
method to get the config dict.
|
method to get the config dict.
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -52,27 +50,22 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = None
|
self.analyze = Analyze(self.config)
|
||||||
self.fiat_converter = None
|
self.fiat_converter = CryptoToFiatConverter()
|
||||||
self.rpc = None
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
self.exchange = None
|
self.exchange = Exchange(self.config)
|
||||||
|
|
||||||
self._init_modules(db_url=db_url)
|
self._init_modules()
|
||||||
|
|
||||||
def _init_modules(self, db_url: Optional[str] = None) -> None:
|
def _init_modules(self) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes all modules and updates the config
|
Initializes all modules and updates the config
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
self.analyze = Analyze(self.config)
|
|
||||||
self.fiat_converter = CryptoToFiatConverter()
|
|
||||||
self.rpc = RPCManager(self)
|
|
||||||
|
|
||||||
persistence.init(self.config, db_url)
|
persistence.init(self.config)
|
||||||
exchange.init(self.config)
|
|
||||||
|
|
||||||
# Set initial application state
|
# Set initial application state
|
||||||
initial_state = self.config.get('initial_state')
|
initial_state = self.config.get('initial_state')
|
||||||
@ -82,19 +75,16 @@ 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: None) -> State:
|
def worker(self, old_state: State = None) -> State:
|
||||||
"""
|
"""
|
||||||
Trading routine that must be run at each loop
|
Trading routine that must be run at each loop
|
||||||
:param old_state: the previous service state from the previous call
|
:param old_state: the previous service state from the previous call
|
||||||
@ -103,7 +93,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:
|
||||||
@ -173,66 +163,74 @@ class FreqtradeBot(object):
|
|||||||
self.check_handle_timedout(self.config['unfilledtimeout'])
|
self.check_handle_timedout(self.config['unfilledtimeout'])
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
except TemporaryError as error:
|
||||||
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
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]:
|
||||||
"""
|
"""
|
||||||
Updates the whitelist with with a dynamically generated list
|
Updates the whitelist with with a dynamically generated list
|
||||||
:param base_currency: base currency as str
|
:param base_currency: base currency as str
|
||||||
:param key: sort key (defaults to 'BaseVolume')
|
:param key: sort key (defaults to 'quoteVolume')
|
||||||
:return: List of pairs
|
:return: List of pairs
|
||||||
"""
|
"""
|
||||||
summaries = sorted(
|
|
||||||
(s for s in exchange.get_market_summaries() if
|
|
||||||
s['MarketName'].startswith(base_currency)),
|
|
||||||
key=lambda s: s.get(key) or 0.0,
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return [s['MarketName'].replace('-', '_') for s in summaries]
|
if not self.exchange.exchange_has('fetchTickers'):
|
||||||
|
raise OperationalException(
|
||||||
|
'Exchange does not support dynamic whitelist.'
|
||||||
|
'Please edit your config and restart the bot'
|
||||||
|
)
|
||||||
|
|
||||||
|
tickers = self.exchange.get_tickers()
|
||||||
|
# check length so that we make sure that '/' is actually in the string
|
||||||
|
tickers = [v for k, v in tickers.items()
|
||||||
|
if len(k.split('/')) == 2 and k.split('/')[1] == base_currency]
|
||||||
|
|
||||||
|
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
|
||||||
|
pairs = [s['symbol'] for s in sorted_tickers]
|
||||||
|
return pairs
|
||||||
|
|
||||||
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
|
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Check wallet health and remove pair from whitelist if necessary
|
Check available markets and remove pair from whitelist if necessary
|
||||||
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
|
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
|
||||||
trade
|
trade
|
||||||
:return: the list of pairs the user wants to trade without the one unavailable or
|
:return: the list of pairs the user wants to trade without the one unavailable or
|
||||||
black_listed
|
black_listed
|
||||||
"""
|
"""
|
||||||
sanitized_whitelist = whitelist
|
sanitized_whitelist = whitelist
|
||||||
health = exchange.get_wallet_health()
|
markets = self.exchange.get_markets()
|
||||||
|
|
||||||
|
markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
|
||||||
known_pairs = set()
|
known_pairs = set()
|
||||||
for status in health:
|
for market in markets:
|
||||||
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
|
pair = market['symbol']
|
||||||
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
||||||
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
|
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
|
||||||
continue
|
continue
|
||||||
# else the pair is valid
|
# else the pair is valid
|
||||||
known_pairs.add(pair)
|
known_pairs.add(pair)
|
||||||
# Market is not active
|
# Market is not active
|
||||||
if not status['IsActive']:
|
if not market['active']:
|
||||||
sanitized_whitelist.remove(pair)
|
sanitized_whitelist.remove(pair)
|
||||||
logger.info(
|
logger.info(
|
||||||
'Ignoring %s from whitelist (reason: %s).',
|
'Ignoring %s from whitelist. Market is not active.',
|
||||||
pair, status.get('Notice') or 'wallet is not active'
|
pair
|
||||||
)
|
)
|
||||||
|
|
||||||
# We need to remove pairs that are unknown
|
# We need to remove pairs that are unknown
|
||||||
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
||||||
|
|
||||||
return final_list
|
return final_list
|
||||||
|
|
||||||
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
||||||
@ -246,27 +244,75 @@ class FreqtradeBot(object):
|
|||||||
balance = self.config['bid_strategy']['ask_last_balance']
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
def _get_trade_stake_amount(self) -> Optional[float]:
|
||||||
|
stake_amount = self.config['stake_amount']
|
||||||
|
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
||||||
|
|
||||||
|
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
|
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
|
||||||
|
if open_trades >= self.config['max_open_trades']:
|
||||||
|
logger.warning('Can\'t open a new trade: max number of trades is reached')
|
||||||
|
return None
|
||||||
|
return avaliable_amount / (self.config['max_open_trades'] - open_trades)
|
||||||
|
|
||||||
|
# Check if stake_amount is fulfilled
|
||||||
|
if avaliable_amount < stake_amount:
|
||||||
|
raise DependencyException(
|
||||||
|
'Available balance(%f %s) is lower than stake amount(%f %s)' % (
|
||||||
|
avaliable_amount, self.config['stake_currency'],
|
||||||
|
stake_amount, self.config['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
|
return stake_amount
|
||||||
|
|
||||||
|
def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]:
|
||||||
|
markets = self.exchange.get_markets()
|
||||||
|
markets = [m for m in markets if m['symbol'] == pair]
|
||||||
|
if not markets:
|
||||||
|
raise ValueError(f'Can\'t get market information for symbol {pair}')
|
||||||
|
|
||||||
|
market = markets[0]
|
||||||
|
|
||||||
|
if 'limits' not in market:
|
||||||
|
return None
|
||||||
|
|
||||||
|
min_stake_amounts = []
|
||||||
|
if 'cost' in market['limits'] and 'min' in market['limits']['cost']:
|
||||||
|
min_stake_amounts.append(market['limits']['cost']['min'])
|
||||||
|
|
||||||
|
if 'amount' in market['limits'] and 'min' in market['limits']['amount']:
|
||||||
|
min_stake_amounts.append(market['limits']['amount']['min'] * price)
|
||||||
|
|
||||||
|
if not min_stake_amounts:
|
||||||
|
return None
|
||||||
|
|
||||||
|
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
|
||||||
|
if self.analyze.get_stoploss() is not None:
|
||||||
|
amount_reserve_percent += self.analyze.get_stoploss()
|
||||||
|
# it should not be more than 50%
|
||||||
|
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||||
|
return min(min_stake_amounts)/amount_reserve_percent
|
||||||
|
|
||||||
def create_trade(self) -> bool:
|
def create_trade(self) -> bool:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
"""
|
"""
|
||||||
stake_amount = self.config['stake_amount']
|
|
||||||
interval = self.analyze.get_ticker_interval()
|
interval = self.analyze.get_ticker_interval()
|
||||||
|
stake_amount = self._get_trade_stake_amount()
|
||||||
|
|
||||||
|
if not stake_amount:
|
||||||
|
return False
|
||||||
|
stake_currency = self.config['stake_currency']
|
||||||
|
fiat_currency = self.config['fiat_display_currency']
|
||||||
|
exc_name = self.exchange.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 ...',
|
||||||
stake_amount
|
stake_amount
|
||||||
)
|
)
|
||||||
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
|
||||||
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
|
|
||||||
raise DependencyException(
|
|
||||||
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
@ -277,50 +323,56 @@ class FreqtradeBot(object):
|
|||||||
if not whitelist:
|
if not whitelist:
|
||||||
raise DependencyException('No currency pairs in whitelist')
|
raise DependencyException('No currency pairs in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
(buy, sell) = self.analyze.get_signal(_pair, interval)
|
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
pair_s = pair.replace('_', '/')
|
||||||
|
pair_url = self.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(self.exchange.get_ticker(pair))
|
||||||
|
|
||||||
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
||||||
|
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||||
|
logger.warning(
|
||||||
|
f'Can\'t open a new trade for {pair_s}: stake amount'
|
||||||
|
f' is too small ({stake_amount} < {min_stake_amount})'
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
amount = stake_amount / buy_limit
|
amount = stake_amount / buy_limit
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
||||||
|
|
||||||
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().upper(),
|
{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 = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
fee=exchange.get_fee(),
|
fee_open=fee,
|
||||||
|
fee_close=fee,
|
||||||
open_rate=buy_limit,
|
open_rate=buy_limit,
|
||||||
|
open_rate_requested=buy_limit,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=exchange.get_name().upper(),
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id
|
open_order_id=order_id
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
@ -348,32 +400,91 @@ class FreqtradeBot(object):
|
|||||||
Tries to execute a sell trade
|
Tries to execute a sell trade
|
||||||
:return: True if executed
|
:return: True if executed
|
||||||
"""
|
"""
|
||||||
# Get order details for actual price per unit
|
try:
|
||||||
if trade.open_order_id:
|
# Get order details for actual price per unit
|
||||||
# Update trade with order values
|
if trade.open_order_id:
|
||||||
logger.info('Found open order for %s', trade)
|
# Update trade with order values
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
logger.info('Found open order for %s', trade)
|
||||||
|
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
|
# Try update amount (binance-fix)
|
||||||
|
try:
|
||||||
|
new_amount = self.get_real_amount(trade, order)
|
||||||
|
if order['amount'] != new_amount:
|
||||||
|
order['amount'] = new_amount
|
||||||
|
# Fee was applied, so set to 0
|
||||||
|
trade.fee_open = 0
|
||||||
|
|
||||||
if trade.is_open and trade.open_order_id is None:
|
except OperationalException as exception:
|
||||||
# Check if we can sell our current pair
|
logger.warning("could not update trade amount: %s", exception)
|
||||||
return self.handle_trade(trade)
|
|
||||||
|
trade.update(order)
|
||||||
|
|
||||||
|
if trade.is_open and trade.open_order_id is None:
|
||||||
|
# Check if we can sell our current pair
|
||||||
|
return self.handle_trade(trade)
|
||||||
|
except DependencyException as exception:
|
||||||
|
logger.warning('Unable to sell trade: %s', exception)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_real_amount(self, trade: Trade, order: Dict) -> float:
|
||||||
|
"""
|
||||||
|
Get real amount for the trade
|
||||||
|
Necessary for self.exchanges which charge fees in base currency (e.g. binance)
|
||||||
|
"""
|
||||||
|
order_amount = order['amount']
|
||||||
|
# Only run for closed orders
|
||||||
|
if trade.fee_open == 0 or order['status'] == 'open':
|
||||||
|
return order_amount
|
||||||
|
|
||||||
|
# use fee from order-dict if possible
|
||||||
|
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
|
||||||
|
if trade.pair.startswith(order['fee']['currency']):
|
||||||
|
new_amount = order_amount - order['fee']['cost']
|
||||||
|
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
|
||||||
|
trade, order['amount'], new_amount)
|
||||||
|
return new_amount
|
||||||
|
|
||||||
|
# Fallback to Trades
|
||||||
|
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
|
||||||
|
trade.open_date)
|
||||||
|
|
||||||
|
if len(trades) == 0:
|
||||||
|
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||||
|
return order_amount
|
||||||
|
amount = 0
|
||||||
|
fee_abs = 0
|
||||||
|
for exectrade in trades:
|
||||||
|
amount += exectrade['amount']
|
||||||
|
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
|
||||||
|
# only applies if fee is in quote currency!
|
||||||
|
if trade.pair.startswith(exectrade['fee']['currency']):
|
||||||
|
fee_abs += exectrade['fee']['cost']
|
||||||
|
|
||||||
|
if amount != order_amount:
|
||||||
|
logger.warning(f"amount {amount} does not match amount {trade.amount}")
|
||||||
|
raise OperationalException("Half bought? Amounts don't match")
|
||||||
|
real_amount = amount - fee_abs
|
||||||
|
if fee_abs != 0:
|
||||||
|
logger.info(f"""Applying fee on amount for {trade} \
|
||||||
|
(from {order_amount} to {real_amount}) from Trades""")
|
||||||
|
return real_amount
|
||||||
|
|
||||||
def handle_trade(self, trade: Trade) -> bool:
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
"""
|
"""
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
Sells the current pair if the threshold is reached and updates the trade record.
|
||||||
: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 = self.exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
experimental = self.config.get('experimental', {})
|
||||||
if self.config.get('experimental', {}).get('use_sell_signal'):
|
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||||
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
(buy, sell) = self.analyze.get_signal(self.exchange,
|
||||||
|
trade.pair, self.analyze.get_ticker_interval())
|
||||||
|
|
||||||
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
||||||
self.execute_sell(trade, current_rate)
|
self.execute_sell(trade, current_rate)
|
||||||
@ -391,22 +502,28 @@ 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:
|
||||||
order = exchange.get_order(trade.open_order_id)
|
# 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 = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Cannot query order for %s due to %s',
|
'Cannot query order for %s due to %s',
|
||||||
trade,
|
trade,
|
||||||
traceback.format_exc())
|
traceback.format_exc())
|
||||||
continue
|
continue
|
||||||
ordertime = arrow.get(order['opened'])
|
ordertime = arrow.get(order['datetime']).datetime
|
||||||
|
|
||||||
# Check if trade is still actually open
|
# Check if trade is still actually open
|
||||||
if int(order['remaining']) == 0:
|
if int(order['remaining']) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
if order['side'] == 'buy' and ordertime < timeoutthreashold:
|
||||||
self.handle_timedout_limit_buy(trade, order)
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
|
||||||
self.handle_timedout_limit_sell(trade, order)
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
@ -416,16 +533,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
|
||||||
"""
|
"""
|
||||||
exchange.cancel_order(trade.open_order_id)
|
pair_s = trade.pair.replace('_', '/')
|
||||||
|
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
if order['remaining'] == order['amount']:
|
if order['remaining'] == order['amount']:
|
||||||
# if trade is not partially completed, just delete the trade
|
# if trade is not partially completed, just delete the trade
|
||||||
Trade.session.delete(trade)
|
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
|
||||||
@ -434,8 +549,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?
|
||||||
@ -444,16 +558,16 @@ 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)
|
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
trade.close_profit = None
|
trade.close_profit = None
|
||||||
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
|
||||||
|
|
||||||
@ -467,51 +581,42 @@ 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)
|
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
|
trade.close_rate_requested = limit
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||||
profit_trade = trade.calc_profit(rate=limit)
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = trade.calc_profit_percent(current_rate)
|
profit = trade.calc_profit_percent(limit)
|
||||||
|
pair_url = self.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}`' \
|
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||||
'` / {profit_fiat:.3f} {fiat})`' \
|
f'` / {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'],
|
|
||||||
)
|
|
||||||
# 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:
|
||||||
|
@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
|
|||||||
return series < series.shift(1)
|
return series < series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
|
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
|
||||||
magic = pi * sqrt(2) / smoothing
|
magic = pi * sqrt(2) / smoothing
|
||||||
a1 = exp(-magic)
|
a1 = exp(-magic)
|
||||||
coeff2 = 2 * a1 * cos(magic)
|
coeff2 = 2 * a1 * cos(magic)
|
||||||
|
@ -5,11 +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.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')
|
||||||
|
|
||||||
@ -43,24 +46,48 @@ 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 ...')
|
||||||
return_code = 0
|
return_code = 0
|
||||||
|
except OperationalException as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
return_code = 2
|
||||||
except BaseException:
|
except BaseException:
|
||||||
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
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ Various tool function for Freqtrade and scripts
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import gzip
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
@ -63,12 +64,28 @@ def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
|||||||
return np.sort(arr, axis=0)
|
return np.sort(arr, axis=0)
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename, data) -> None:
|
def file_dump_json(filename, data, is_zip=False) -> None:
|
||||||
"""
|
"""
|
||||||
Dump JSON data into a file
|
Dump JSON data into a file
|
||||||
:param filename: file to create
|
:param filename: file to create
|
||||||
:param data: JSON Data to save
|
:param data: JSON Data to save
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
with open(filename, 'w') as fp:
|
print(f'dumping json to "{filename}"')
|
||||||
json.dump(data, fp, default=str)
|
|
||||||
|
if is_zip:
|
||||||
|
if not filename.endswith('.gz'):
|
||||||
|
filename = filename + '.gz'
|
||||||
|
with gzip.open(filename, 'w') as fp:
|
||||||
|
json.dump(data, fp, default=str)
|
||||||
|
else:
|
||||||
|
with open(filename, 'w') as fp:
|
||||||
|
json.dump(data, fp, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
def format_ms_time(date: int) -> str:
|
||||||
|
"""
|
||||||
|
convert MS date to readable format.
|
||||||
|
: epoch-string in ms
|
||||||
|
"""
|
||||||
|
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')
|
||||||
|
@ -4,38 +4,59 @@ import gzip
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List, Dict, Tuple
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
|
import arrow
|
||||||
|
|
||||||
from freqtrade import misc
|
from freqtrade import misc, constants, OperationalException
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import Exchange
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
from freqtrade.arguments import TimeRange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
|
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||||
stype, start, stop = timerange
|
if not tickerlist:
|
||||||
if stype == (None, 'line'):
|
return tickerlist
|
||||||
return tickerlist[stop:]
|
|
||||||
elif stype == ('line', None):
|
|
||||||
return tickerlist[0:start]
|
|
||||||
elif stype == ('index', 'index'):
|
|
||||||
return tickerlist[start:stop]
|
|
||||||
|
|
||||||
return tickerlist
|
start_index = 0
|
||||||
|
stop_index = len(tickerlist)
|
||||||
|
|
||||||
|
if timerange.starttype == 'line':
|
||||||
|
stop_index = timerange.startts
|
||||||
|
if timerange.starttype == 'index':
|
||||||
|
start_index = timerange.startts
|
||||||
|
elif timerange.starttype == 'date':
|
||||||
|
while (start_index < len(tickerlist) and
|
||||||
|
tickerlist[start_index][0] < timerange.startts * 1000):
|
||||||
|
start_index += 1
|
||||||
|
|
||||||
|
if timerange.stoptype == 'line':
|
||||||
|
start_index = len(tickerlist) + timerange.stopts
|
||||||
|
if timerange.stoptype == 'index':
|
||||||
|
stop_index = timerange.stopts
|
||||||
|
elif timerange.stoptype == 'date':
|
||||||
|
while (stop_index > 0 and
|
||||||
|
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
|
||||||
|
stop_index -= 1
|
||||||
|
|
||||||
|
if start_index > stop_index:
|
||||||
|
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
|
||||||
|
|
||||||
|
return tickerlist[start_index:stop_index]
|
||||||
|
|
||||||
|
|
||||||
def load_tickerdata_file(
|
def load_tickerdata_file(
|
||||||
datadir: str, pair: str,
|
datadir: str, pair: str,
|
||||||
ticker_interval: int,
|
ticker_interval: str,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
|
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
|
||||||
"""
|
"""
|
||||||
Load a pair from file,
|
Load a pair from file,
|
||||||
:return dict OR empty if unsuccesful
|
:return dict OR empty if unsuccesful
|
||||||
"""
|
"""
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
|
pair_file_string = pair.replace('/', '_')
|
||||||
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
|
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
|
||||||
pair=pair,
|
pair=pair_file_string,
|
||||||
ticker_interval=ticker_interval,
|
ticker_interval=ticker_interval,
|
||||||
))
|
))
|
||||||
gzipfile = file + '.gz'
|
gzipfile = file + '.gz'
|
||||||
@ -58,31 +79,38 @@ def load_tickerdata_file(
|
|||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
def load_data(datadir: str, ticker_interval: int,
|
def load_data(datadir: str,
|
||||||
pairs: Optional[List[str]] = None,
|
ticker_interval: str,
|
||||||
|
pairs: List[str],
|
||||||
refresh_pairs: Optional[bool] = False,
|
refresh_pairs: Optional[bool] = False,
|
||||||
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
|
exchange: Optional[Exchange] = None,
|
||||||
|
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
|
||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
# If the user force the refresh of pairs
|
# If the user force the refresh of pairs
|
||||||
if refresh_pairs:
|
if refresh_pairs:
|
||||||
logger.info('Download data for all pairs and store them in %s', datadir)
|
logger.info('Download data for all pairs and store them in %s', datadir)
|
||||||
download_pairs(datadir, _pairs, ticker_interval)
|
if not exchange:
|
||||||
|
raise OperationalException("Exchange needs to be initialized when "
|
||||||
|
"calling load_data with refresh_pairs=True")
|
||||||
|
download_pairs(datadir, exchange, pairs, ticker_interval, timerange=timerange)
|
||||||
|
|
||||||
for pair in _pairs:
|
for pair in pairs:
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
if not pairdata:
|
if pairdata:
|
||||||
# download the tickerdata from exchange
|
result[pair] = pairdata
|
||||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
else:
|
||||||
# and retry reading the pair
|
logger.warning(
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
'No data for pair: "%s", Interval: %s. '
|
||||||
result[pair] = pairdata
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
pair,
|
||||||
|
ticker_interval
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -95,14 +123,20 @@ def make_testdata_path(datadir: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
def download_pairs(datadir, exchange: Exchange, pairs: List[str],
|
||||||
|
ticker_interval: str,
|
||||||
|
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:
|
||||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
download_backtesting_testdata(datadir,
|
||||||
|
exchange=exchange,
|
||||||
|
pair=pair,
|
||||||
|
tick_interval=ticker_interval,
|
||||||
|
timerange=timerange)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info(
|
logger.info(
|
||||||
'Failed to download the pair: "%s", Interval: %s min',
|
'Failed to download the pair: "%s", Interval: %s',
|
||||||
pair,
|
pair,
|
||||||
ticker_interval
|
ticker_interval
|
||||||
)
|
)
|
||||||
@ -110,39 +144,89 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# FIX: 20180110, suggest rename interval to tick_interval
|
def load_cached_data_for_updating(filename: str,
|
||||||
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> None:
|
tick_interval: str,
|
||||||
|
timerange: Optional[TimeRange]) -> Tuple[
|
||||||
|
List[Any],
|
||||||
|
Optional[int]]:
|
||||||
"""
|
"""
|
||||||
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
|
Load cached data and choose what part of the data should be updated
|
||||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = make_testdata_path(datadir)
|
since_ms = None
|
||||||
logger.info(
|
|
||||||
'Download the pair: "%s", Interval: %s min', pair, interval
|
|
||||||
)
|
|
||||||
|
|
||||||
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
# user sets timerange, so find the start time
|
||||||
pair=pair.replace("-", "_"),
|
if timerange:
|
||||||
interval=interval,
|
if timerange.starttype == 'date':
|
||||||
))
|
since_ms = timerange.startts * 1000
|
||||||
|
elif timerange.stoptype == 'line':
|
||||||
|
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||||
|
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
|
||||||
|
|
||||||
|
# read the cached file
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename, "rt") as file:
|
with open(filename, "rt") as file:
|
||||||
data = json.load(file)
|
data = json.load(file)
|
||||||
|
# remove the last item, because we are not sure if it is correct
|
||||||
|
# it could be fetched when the candle was incompleted
|
||||||
|
if data:
|
||||||
|
data.pop()
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
logger.debug('Current Start: %s', data[1]['T'] if data else None)
|
if data:
|
||||||
logger.debug('Current End: %s', data[-1:][0]['T'] if data else None)
|
if since_ms and since_ms < data[0][0]:
|
||||||
|
# the data is requested for earlier period than the cache has
|
||||||
|
# so fully redownload all the data
|
||||||
|
data = []
|
||||||
|
else:
|
||||||
|
# a part of the data was already downloaded, so
|
||||||
|
# download unexist data only
|
||||||
|
since_ms = data[-1][0] + 1
|
||||||
|
|
||||||
# Extend data with new ticker history
|
return (data, since_ms)
|
||||||
data.extend([
|
|
||||||
row for row in get_ticker_history(pair=pair, tick_interval=int(interval))
|
|
||||||
if row not in data
|
def download_backtesting_testdata(datadir: str,
|
||||||
])
|
exchange: Exchange,
|
||||||
|
pair: str,
|
||||||
|
tick_interval: str = '5m',
|
||||||
|
timerange: Optional[TimeRange] = None) -> None:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
||||||
|
The data is downloaded starting from the last correct ticker interval data that
|
||||||
|
esists in a cache. If timerange starts earlier than the data in the cache,
|
||||||
|
the full data will be redownloaded
|
||||||
|
|
||||||
|
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||||
|
:param pairs: list of pairs to download
|
||||||
|
:param tick_interval: ticker interval
|
||||||
|
:param timerange: range of time to download
|
||||||
|
:return: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = make_testdata_path(datadir)
|
||||||
|
filepair = pair.replace("/", "_")
|
||||||
|
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Download the pair: "%s", Interval: %s',
|
||||||
|
pair,
|
||||||
|
tick_interval
|
||||||
|
)
|
||||||
|
|
||||||
|
data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange)
|
||||||
|
|
||||||
|
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
||||||
|
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||||
|
|
||||||
|
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
|
||||||
|
since_ms=since_ms)
|
||||||
|
data.extend(new_data)
|
||||||
|
|
||||||
|
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
||||||
|
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
|
||||||
|
|
||||||
data = sorted(data, key=lambda _data: _data['T'])
|
|
||||||
logger.debug('New Start: %s', data[1]['T'])
|
|
||||||
logger.debug('New End: %s', data[-1:][0]['T'])
|
|
||||||
misc.file_dump_json(filename, data)
|
misc.file_dump_json(filename, data)
|
||||||
|
@ -6,25 +6,40 @@ This module contains the backtesting logic
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, Tuple, Any, List, Optional
|
from datetime import datetime
|
||||||
|
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import exchange
|
from freqtrade import constants, DependencyException
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Bittrex
|
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BacktestResult(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple Defining BacktestResults inputs.
|
||||||
|
"""
|
||||||
|
pair: str
|
||||||
|
profit_percent: float
|
||||||
|
profit_abs: float
|
||||||
|
open_time: datetime
|
||||||
|
close_time: datetime
|
||||||
|
open_index: int
|
||||||
|
close_index: int
|
||||||
|
trade_duration: float
|
||||||
|
open_at_end: bool
|
||||||
|
|
||||||
|
|
||||||
class Backtesting(object):
|
class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
Backtesting class, this class contains all the logic to run a backtest
|
Backtesting class, this class contains all the logic to run a backtest
|
||||||
@ -35,24 +50,20 @@ class Backtesting(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = None
|
|
||||||
self.ticker_interval = None
|
|
||||||
self.tickerdata_to_dataframe = None
|
|
||||||
self.populate_buy_trend = None
|
|
||||||
self.populate_sell_trend = None
|
|
||||||
self._init()
|
|
||||||
|
|
||||||
def _init(self) -> None:
|
|
||||||
"""
|
|
||||||
Init objects required for backtesting
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.analyze = Analyze(self.config)
|
self.analyze = Analyze(self.config)
|
||||||
self.ticker_interval = self.analyze.strategy.ticker_interval
|
self.ticker_interval = self.analyze.strategy.ticker_interval
|
||||||
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
||||||
self.populate_buy_trend = self.analyze.populate_buy_trend
|
self.populate_buy_trend = self.analyze.populate_buy_trend
|
||||||
self.populate_sell_trend = self.analyze.populate_sell_trend
|
self.populate_sell_trend = self.analyze.populate_sell_trend
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
# Reset keys for backtesting
|
||||||
|
self.config['exchange']['key'] = ''
|
||||||
|
self.config['exchange']['secret'] = ''
|
||||||
|
self.config['exchange']['password'] = ''
|
||||||
|
self.config['exchange']['uid'] = ''
|
||||||
|
self.config['dry_run'] = True
|
||||||
|
self.exchange = Exchange(self.config)
|
||||||
|
self.fee = self.exchange.get_fee()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
@ -73,22 +84,22 @@ class Backtesting(object):
|
|||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
:return: pretty printed table with tabulate as str
|
:return: pretty printed table with tabulate as str
|
||||||
"""
|
"""
|
||||||
stake_currency = self.config.get('stake_currency')
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['pair', 'buy count', 'avg profit %',
|
headers = ['pair', 'buy count', 'avg profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.currency == pair]
|
result = results[results.pair == pair]
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
result.profit_percent.mean() * 100.0,
|
||||||
result.profit_BTC.sum(),
|
result.profit_abs.sum(),
|
||||||
result.duration.mean(),
|
result.trade_duration.mean(),
|
||||||
len(result[result.profit_BTC > 0]),
|
len(result[result.profit_abs > 0]),
|
||||||
len(result[result.profit_BTC < 0])
|
len(result[result.profit_abs < 0])
|
||||||
])
|
])
|
||||||
|
|
||||||
# Append Total
|
# Append Total
|
||||||
@ -96,16 +107,28 @@ class Backtesting(object):
|
|||||||
'TOTAL',
|
'TOTAL',
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_abs.sum(),
|
||||||
results.duration.mean(),
|
results.trade_duration.mean(),
|
||||||
len(results[results.profit_BTC > 0]),
|
len(results[results.profit_abs > 0]),
|
||||||
len(results[results.profit_BTC < 0])
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
|
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
|
||||||
|
|
||||||
|
records = [(trade_entry.pair, trade_entry.profit_percent,
|
||||||
|
trade_entry.open_time.timestamp(),
|
||||||
|
trade_entry.close_time.timestamp(),
|
||||||
|
trade_entry.open_index - 1, trade_entry.trade_duration)
|
||||||
|
for index, trade_entry in results.iterrows()]
|
||||||
|
|
||||||
|
if records:
|
||||||
|
logger.info('Dumping backtest results to %s', recordfilename)
|
||||||
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
def _get_sell_trade_entry(
|
def _get_sell_trade_entry(
|
||||||
self, pair: str, buy_row: DataFrame,
|
self, pair: str, buy_row: DataFrame,
|
||||||
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
|
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
|
||||||
|
|
||||||
stake_amount = args['stake_amount']
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
@ -114,7 +137,8 @@ class Backtesting(object):
|
|||||||
open_date=buy_row.date,
|
open_date=buy_row.date,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=stake_amount / buy_row.open,
|
amount=stake_amount / buy_row.open,
|
||||||
fee=exchange.get_fee()
|
fee_open=self.fee,
|
||||||
|
fee_close=self.fee
|
||||||
)
|
)
|
||||||
|
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
@ -126,15 +150,33 @@ class Backtesting(object):
|
|||||||
buy_signal = sell_row.buy
|
buy_signal = sell_row.buy
|
||||||
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||||
sell_row.sell):
|
sell_row.sell):
|
||||||
return \
|
|
||||||
sell_row, \
|
return BacktestResult(pair=pair,
|
||||||
(
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
pair,
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
trade.calc_profit_percent(rate=sell_row.close),
|
open_time=buy_row.date,
|
||||||
trade.calc_profit(rate=sell_row.close),
|
close_time=sell_row.date,
|
||||||
(sell_row.date - buy_row.date).seconds // 60
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
), \
|
open_index=buy_row.Index,
|
||||||
sell_row.date
|
close_index=sell_row.Index,
|
||||||
|
open_at_end=False
|
||||||
|
)
|
||||||
|
if partial_ticker:
|
||||||
|
# no sell condition found - trade stil open at end of backtest period
|
||||||
|
sell_row = partial_ticker[-1]
|
||||||
|
btr = BacktestResult(pair=pair,
|
||||||
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
|
open_time=buy_row.date,
|
||||||
|
close_time=sell_row.date,
|
||||||
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
|
open_index=buy_row.Index,
|
||||||
|
close_index=sell_row.Index,
|
||||||
|
open_at_end=True
|
||||||
|
)
|
||||||
|
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||||
|
btr.profit_percent, btr.profit_abs)
|
||||||
|
return btr
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def backtest(self, args: Dict) -> DataFrame:
|
def backtest(self, args: Dict) -> DataFrame:
|
||||||
@ -150,22 +192,28 @@ class Backtesting(object):
|
|||||||
processed: a processed dictionary with format {pair, data}
|
processed: a processed dictionary with format {pair, data}
|
||||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
realistic: do we try to simulate realistic trades? (default: True)
|
realistic: do we try to simulate realistic trades? (default: True)
|
||||||
sell_profit_only: sell if profit only
|
|
||||||
use_sell_signal: act on sell-signal
|
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
realistic = args.get('realistic', False)
|
realistic = args.get('realistic', False)
|
||||||
record = args.get('record', None)
|
|
||||||
records = []
|
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock = {}
|
trade_count_lock: Dict = {}
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
|
ticker_data = self.populate_sell_trend(
|
||||||
|
self.populate_buy_trend(pair_data))[headers].copy()
|
||||||
|
|
||||||
|
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||||
|
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||||
|
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
|
||||||
|
|
||||||
|
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||||
|
|
||||||
|
# Convert from Pandas to list for performance reasons
|
||||||
|
# (Looping Pandas is slow.)
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
lock_pair_until = None
|
lock_pair_until = None
|
||||||
@ -183,28 +231,18 @@ class Backtesting(object):
|
|||||||
|
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, args)
|
||||||
|
|
||||||
if ret:
|
if trade_entry:
|
||||||
row2, trade_entry, next_date = ret
|
lock_pair_until = trade_entry.close_time
|
||||||
lock_pair_until = next_date
|
|
||||||
trades.append(trade_entry)
|
trades.append(trade_entry)
|
||||||
if record:
|
else:
|
||||||
# Note, need to be json.dump friendly
|
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||||
# record a tuple of pair, current_profit_percent,
|
# This happens only if the buy-signal was with the last candle
|
||||||
# entry-date, duration
|
lock_pair_until = ticker_data.iloc[-1].date
|
||||||
records.append((pair, trade_entry[1],
|
|
||||||
row.date.strftime('%s'),
|
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||||
row2.date.strftime('%s'),
|
|
||||||
index, trade_entry[3]))
|
|
||||||
# For now export inside backtest(), maybe change so that backtest()
|
|
||||||
# returns a tuple like: (dataframe, records, logs, etc)
|
|
||||||
if record and record.find('trades') >= 0:
|
|
||||||
logger.info('Dumping backtest results')
|
|
||||||
file_dump_json('backtest-result.json', records)
|
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -219,19 +257,24 @@ class Backtesting(object):
|
|||||||
if self.config.get('live'):
|
if self.config.get('live'):
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
|
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
|
||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = optimize.load_data(
|
data = optimize.load_data(
|
||||||
self.config['datadir'],
|
self.config['datadir'],
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
ticker_interval=self.ticker_interval,
|
ticker_interval=self.ticker_interval,
|
||||||
refresh_pairs=self.config.get('refresh_pairs', False),
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
|
exchange=self.exchange,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
logger.critical("No data found. Terminating.")
|
||||||
|
return
|
||||||
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
||||||
if self.config.get('realistic_simulation', False):
|
if self.config.get('realistic_simulation', False):
|
||||||
max_open_trades = self.config['max_open_trades']
|
max_open_trades = self.config['max_open_trades']
|
||||||
@ -251,23 +294,22 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
|
|
||||||
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
|
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config.get('stake_amount'),
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
'processed': preprocessed,
|
'processed': preprocessed,
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'realistic': self.config.get('realistic_simulation', False),
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
'sell_profit_only': sell_profit_only,
|
|
||||||
'use_sell_signal': use_sell_signal,
|
|
||||||
'record': self.config.get('export')
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.config.get('export', False):
|
||||||
|
self._store_backtest_result(self.config.get('exportfilename'), results)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'\n==================================== '
|
'\n======================================== '
|
||||||
'BACKTESTING REPORT'
|
'BACKTESTING REPORT'
|
||||||
' ====================================\n'
|
' =========================================\n'
|
||||||
'%s',
|
'%s',
|
||||||
self._generate_text_table(
|
self._generate_text_table(
|
||||||
data,
|
data,
|
||||||
@ -275,6 +317,17 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'\n====================================== '
|
||||||
|
'LEFT OPEN TRADES REPORT'
|
||||||
|
' ======================================\n'
|
||||||
|
'%s',
|
||||||
|
self._generate_text_table(
|
||||||
|
data,
|
||||||
|
results.loc[results.open_at_end]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -289,6 +342,10 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
|||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
|
raise DependencyException('stake amount could not be "%s" for backtesting' %
|
||||||
|
constants.UNLIMITED_STAKE_AMOUNT)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,12 +14,11 @@ from argparse import Namespace
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import exp
|
from math import exp
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Dict, Any, Callable
|
from typing import Dict, Any, Callable, Optional
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
||||||
from hyperopt.mongoexp import MongoTrials
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
@ -27,8 +26,6 @@ from freqtrade.arguments import Arguments
|
|||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.optimize import load_data
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -42,7 +39,6 @@ class Hyperopt(Backtesting):
|
|||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||||
# to the number of days
|
# to the number of days
|
||||||
@ -61,7 +57,7 @@ class Hyperopt(Backtesting):
|
|||||||
self.expected_max_profit = 3.0
|
self.expected_max_profit = 3.0
|
||||||
|
|
||||||
# Configuration and data used by hyperopt
|
# Configuration and data used by hyperopt
|
||||||
self.processed = None
|
self.processed: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
# Hyperopt Trials
|
# Hyperopt Trials
|
||||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||||
@ -345,7 +341,7 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Return the space to use during Hyperopt
|
Return the space to use during Hyperopt
|
||||||
"""
|
"""
|
||||||
spaces = {}
|
spaces: Dict = {}
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
@ -452,10 +448,11 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results.profit_percent.sum()
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
trade_duration = results.duration.mean()
|
trade_duration = results.trade_duration.mean()
|
||||||
|
|
||||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
|
sys.stdout.flush()
|
||||||
return {
|
return {
|
||||||
'status': STATUS_FAIL,
|
'status': STATUS_FAIL,
|
||||||
'loss': float('inf')
|
'loss': float('inf')
|
||||||
@ -480,59 +477,48 @@ class Hyperopt(Backtesting):
|
|||||||
'result': result_explanation,
|
'result': result_explanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
def format_results(self, results: DataFrame) -> str:
|
||||||
def format_results(results: DataFrame) -> str:
|
|
||||||
"""
|
"""
|
||||||
Return the format result in a string
|
Return the format result in a string
|
||||||
"""
|
"""
|
||||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||||
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_abs.sum(),
|
||||||
|
self.config['stake_currency'],
|
||||||
results.profit_percent.sum(),
|
results.profit_percent.sum(),
|
||||||
results.duration.mean(),
|
results.trade_duration.mean(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=self.config.get('datadir'),
|
datadir=str(self.config.get('datadir')),
|
||||||
pairs=self.config['exchange']['pair_whitelist'],
|
pairs=self.config['exchange']['pair_whitelist'],
|
||||||
ticker_interval=self.ticker_interval,
|
ticker_interval=self.ticker_interval,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.analyze.populate_indicators = Hyperopt.populate_indicators
|
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||||
self.processed = self.tickerdata_to_dataframe(data)
|
self.processed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
if self.config.get('mongodb'):
|
logger.info('Preparing Trials..')
|
||||||
logger.info('Using mongodb ...')
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
# read trials file if we have one
|
||||||
|
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||||
|
self.trials = self.read_trials()
|
||||||
|
|
||||||
|
self.current_tries = len(self.trials.results)
|
||||||
|
self.total_tries += self.current_tries
|
||||||
logger.info(
|
logger.info(
|
||||||
'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!'
|
'Continuing with trials. Current: %d, Total: %d',
|
||||||
|
self.current_tries,
|
||||||
|
self.total_tries
|
||||||
)
|
)
|
||||||
|
|
||||||
db_name = 'freqtrade_hyperopt'
|
|
||||||
self.trials = MongoTrials(
|
|
||||||
arg='mongo://127.0.0.1:1234/{}/jobs'.format(db_name),
|
|
||||||
exp_key='exp1'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info('Preparing Trials..')
|
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
|
||||||
# read trials file if we have one
|
|
||||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
|
||||||
self.trials = self.read_trials()
|
|
||||||
|
|
||||||
self.current_tries = len(self.trials.results)
|
|
||||||
self.total_tries += self.current_tries
|
|
||||||
logger.info(
|
|
||||||
'Continuing with trials. Current: %d, Total: %d',
|
|
||||||
self.current_tries,
|
|
||||||
self.total_tries
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
best_parameters = fmin(
|
best_parameters = fmin(
|
||||||
fn=self.generate_optimizer,
|
fn=self.generate_optimizer,
|
||||||
@ -588,18 +574,14 @@ def start(args: Namespace) -> None:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
# Monkey patch the configuration with hyperopt_conf.py
|
# Monkey patch the configuration with hyperopt_conf.py
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
|
config = configuration.load_config()
|
||||||
|
|
||||||
optimize_config = hyperopt_optimize_conf()
|
|
||||||
config = configuration._load_common_config(optimize_config)
|
|
||||||
config = configuration._load_backtesting_config(config)
|
|
||||||
config = configuration._load_hyperopt_config(config)
|
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
@ -5,57 +5,111 @@ This module contains the class to persist trades into SQLite
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||||
create_engine)
|
create_engine)
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.exc import NoSuchModuleError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONF = {}
|
_DECL_BASE: Any = declarative_base()
|
||||||
_DECL_BASE = declarative_base()
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, engine: Optional[Engine] = None) -> None:
|
def init(config: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
and starts polling for message updates
|
and starts polling for message updates
|
||||||
:param config: config to use
|
:param config: config to use
|
||||||
:param engine: database engine for sqlalchemy (Optional)
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
_CONF.update(config)
|
db_url = config.get('db_url', None)
|
||||||
if not engine:
|
kwargs = {}
|
||||||
if _CONF.get('dry_run', False):
|
|
||||||
# the user wants dry run to use a DB
|
# Take care of thread ownership if in-memory db
|
||||||
if _CONF.get('dry_run_db', False):
|
if db_url == 'sqlite://':
|
||||||
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
|
kwargs.update({
|
||||||
# Otherwise dry run will store in memory
|
'connect_args': {'check_same_thread': False},
|
||||||
else:
|
'poolclass': StaticPool,
|
||||||
engine = create_engine('sqlite://',
|
'echo': False,
|
||||||
connect_args={'check_same_thread': False},
|
})
|
||||||
poolclass=StaticPool,
|
|
||||||
echo=False)
|
try:
|
||||||
else:
|
engine = create_engine(db_url, **kwargs)
|
||||||
engine = create_engine('sqlite:///tradesv3.sqlite')
|
except NoSuchModuleError:
|
||||||
|
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
|
||||||
|
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||||
|
)
|
||||||
|
raise OperationalException(error)
|
||||||
|
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||||
Trade.session = session()
|
Trade.session = session()
|
||||||
Trade.query = session.query_property()
|
Trade.query = session.query_property()
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
check_migrate(engine)
|
||||||
|
|
||||||
# Clean dry_run DB
|
# Clean dry_run DB if the db is not in-memory
|
||||||
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
if config.get('dry_run', False) and db_url != 'sqlite://':
|
||||||
clean_dry_run_db()
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
|
def has_column(columns, searchname: str) -> bool:
|
||||||
|
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def check_migrate(engine) -> None:
|
||||||
|
"""
|
||||||
|
Checks if migration is necessary and migrates if necessary
|
||||||
|
"""
|
||||||
|
inspector = inspect(engine)
|
||||||
|
|
||||||
|
cols = inspector.get_columns('trades')
|
||||||
|
|
||||||
|
if not has_column(cols, 'fee_open'):
|
||||||
|
# Schema migration necessary
|
||||||
|
engine.execute("alter table trades rename to trades_bak")
|
||||||
|
# let SQLAlchemy create the schema as required
|
||||||
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Copy data back - following the correct schema
|
||||||
|
engine.execute("""insert into trades
|
||||||
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
|
stake_amount, amount, open_date, close_date, open_order_id)
|
||||||
|
select id, lower(exchange),
|
||||||
|
case
|
||||||
|
when instr(pair, '_') != 0 then
|
||||||
|
substr(pair, instr(pair, '_') + 1) || '/' ||
|
||||||
|
substr(pair, 1, instr(pair, '_') - 1)
|
||||||
|
else pair
|
||||||
|
end
|
||||||
|
pair,
|
||||||
|
is_open, fee fee_open, fee fee_close,
|
||||||
|
open_rate, null open_rate_requested, close_rate,
|
||||||
|
null close_rate_requested, close_profit,
|
||||||
|
stake_amount, amount, open_date, close_date, open_order_id
|
||||||
|
from trades_bak
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Reread columns - the above recreated the table!
|
||||||
|
inspector = inspect(engine)
|
||||||
|
cols = inspector.get_columns('trades')
|
||||||
|
|
||||||
|
if not has_column(cols, 'open_rate_requested'):
|
||||||
|
engine.execute("alter table trades add open_rate_requested float")
|
||||||
|
if not has_column(cols, 'close_rate_requested'):
|
||||||
|
engine.execute("alter table trades add close_rate_requested float")
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
Flushes all pending operations to disk.
|
Flushes all pending operations to disk.
|
||||||
@ -85,9 +139,12 @@ class Trade(_DECL_BASE):
|
|||||||
exchange = Column(String, nullable=False)
|
exchange = Column(String, nullable=False)
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False)
|
||||||
is_open = Column(Boolean, nullable=False, default=True)
|
is_open = Column(Boolean, nullable=False, default=True)
|
||||||
fee = Column(Float, nullable=False, default=0.0)
|
fee_open = Column(Float, nullable=False, default=0.0)
|
||||||
|
fee_close = Column(Float, nullable=False, default=0.0)
|
||||||
open_rate = Column(Float)
|
open_rate = Column(Float)
|
||||||
|
open_rate_requested = Column(Float)
|
||||||
close_rate = Column(Float)
|
close_rate = Column(Float)
|
||||||
|
close_rate_requested = Column(Float)
|
||||||
close_profit = Column(Float)
|
close_profit = Column(Float)
|
||||||
stake_amount = Column(Float, nullable=False)
|
stake_amount = Column(Float, nullable=False)
|
||||||
amount = Column(Float)
|
amount = Column(Float)
|
||||||
@ -111,20 +168,20 @@ class Trade(_DECL_BASE):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Ignore open and cancelled orders
|
# Ignore open and cancelled orders
|
||||||
if not order['closed'] or order['rate'] is None:
|
if order['status'] == 'open' or order['price'] is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info('Updating trade (id=%d) ...', self.id)
|
logger.info('Updating trade (id=%d) ...', self.id)
|
||||||
|
|
||||||
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
||||||
if order['type'] == 'LIMIT_BUY':
|
if order['type'] == 'limit' and order['side'] == 'buy':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = Decimal(order['rate'])
|
self.open_rate = Decimal(order['price'])
|
||||||
self.amount = Decimal(order['amount'])
|
self.amount = Decimal(order['amount'])
|
||||||
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
elif order['type'] == 'LIMIT_SELL':
|
elif order['type'] == 'limit' and order['side'] == 'sell':
|
||||||
self.close(order['rate'])
|
self.close(order['price'])
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
cleanup()
|
cleanup()
|
||||||
@ -156,7 +213,7 @@ class Trade(_DECL_BASE):
|
|||||||
getcontext().prec = 8
|
getcontext().prec = 8
|
||||||
|
|
||||||
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
||||||
fees = buy_trade * Decimal(fee or self.fee)
|
fees = buy_trade * Decimal(fee or self.fee_open)
|
||||||
return float(buy_trade + fees)
|
return float(buy_trade + fees)
|
||||||
|
|
||||||
def calc_close_trade_price(
|
def calc_close_trade_price(
|
||||||
@ -177,7 +234,7 @@ class Trade(_DECL_BASE):
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
|
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
|
||||||
fees = sell_trade * Decimal(fee or self.fee)
|
fees = sell_trade * Decimal(fee or self.fee_close)
|
||||||
return float(sell_trade - fees)
|
return float(sell_trade - fees)
|
||||||
|
|
||||||
def calc_profit(
|
def calc_profit(
|
||||||
@ -195,7 +252,7 @@ class Trade(_DECL_BASE):
|
|||||||
open_trade_price = self.calc_open_trade_price()
|
open_trade_price = self.calc_open_trade_price()
|
||||||
close_trade_price = self.calc_close_trade_price(
|
close_trade_price = self.calc_close_trade_price(
|
||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee)
|
fee=(fee or self.fee_close)
|
||||||
)
|
)
|
||||||
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
||||||
|
|
||||||
@ -215,7 +272,7 @@ class Trade(_DECL_BASE):
|
|||||||
open_trade_price = self.calc_open_trade_price()
|
open_trade_price = self.calc_open_trade_price()
|
||||||
close_trade_price = self.calc_close_trade_price(
|
close_trade_price = self.calc_close_trade_price(
|
||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee)
|
fee=(fee or self.fee_close)
|
||||||
)
|
)
|
||||||
|
|
||||||
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
||||||
|
@ -2,23 +2,33 @@
|
|||||||
This module contains class to define a RPC communications
|
This module contains class to define a RPC communications
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from abc import abstractmethod
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Tuple, Any
|
from typing import Dict, Tuple, Any, List
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
|
from numpy import mean, nan_to_num
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import exchange
|
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCException(Exception):
|
||||||
|
"""
|
||||||
|
Should be raised with a rpc-formatted message in an _rpc_* method
|
||||||
|
if the required state is wrong, i.e.:
|
||||||
|
|
||||||
|
raise RPCException('*Status:* `no active trade`')
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RPC(object):
|
class RPC(object):
|
||||||
"""
|
"""
|
||||||
RPC class can be used to have extra feature, like bot data, and access to DB data
|
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||||
@ -29,28 +39,40 @@ class RPC(object):
|
|||||||
:param freqtrade: Instance of a freqtrade bot
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.freqtrade = freqtrade
|
self._freqtrade = freqtrade
|
||||||
|
|
||||||
def rpc_trade_status(self) -> Tuple[bool, Any]:
|
@abstractmethod
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
""" Cleanup pending module resources """
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
""" Returns the lowercase name of this module """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_msg(self, msg: str) -> None:
|
||||||
|
""" Sends a message to all registered rpc modules """
|
||||||
|
|
||||||
|
def _rpc_trade_status(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||||
a remotely exposed function
|
a remotely exposed function
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
# Fetch open trade
|
# Fetch open trade
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '*Status:* `trader is not running`'
|
raise RPCException('*Status:* `trader is not running`')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
return True, '*Status:* `no active trade`'
|
raise RPCException('*Status:* `no active trade`')
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
order = None
|
order = None
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
order = exchange.get_order(trade.open_order_id)
|
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
fmt_close_profit = '{:.2f}%'.format(
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
round(trade.close_profit * 100, 2)
|
round(trade.close_profit * 100, 2)
|
||||||
@ -68,7 +90,7 @@ class RPC(object):
|
|||||||
.format(
|
.format(
|
||||||
trade_id=trade.id,
|
trade_id=trade.id,
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
market_url=exchange.get_pair_detail_url(trade.pair),
|
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||||
date=arrow.get(trade.open_date).humanize(),
|
date=arrow.get(trade.open_date).humanize(),
|
||||||
open_rate=trade.open_rate,
|
open_rate=trade.open_rate,
|
||||||
close_rate=trade.close_rate,
|
close_rate=trade.close_rate,
|
||||||
@ -76,24 +98,24 @@ class RPC(object):
|
|||||||
amount=round(trade.amount, 8),
|
amount=round(trade.amount, 8),
|
||||||
close_profit=fmt_close_profit,
|
close_profit=fmt_close_profit,
|
||||||
current_profit=round(current_profit * 100, 2),
|
current_profit=round(current_profit * 100, 2),
|
||||||
open_order='({} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
)
|
)
|
||||||
result.append(message)
|
result.append(message)
|
||||||
return False, result
|
return result
|
||||||
|
|
||||||
def rpc_status_table(self) -> Tuple[bool, Any]:
|
def _rpc_status_table(self) -> DataFrame:
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '*Status:* `trader is not running`'
|
raise RPCException('*Status:* `trader is not running`')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
return True, '*Status:* `no active order`'
|
raise RPCException('*Status:* `no active order`')
|
||||||
else:
|
else:
|
||||||
trades_list = []
|
trades_list = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
trades_list.append([
|
trades_list.append([
|
||||||
trade.id,
|
trade.id,
|
||||||
trade.pair,
|
trade.pair,
|
||||||
@ -104,22 +126,18 @@ class RPC(object):
|
|||||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||||
df_statuses = df_statuses.set_index(columns[0])
|
df_statuses = df_statuses.set_index(columns[0])
|
||||||
# The style used throughout is to return a tuple
|
return df_statuses
|
||||||
# consisting of (error_occured?, result)
|
|
||||||
# Another approach would be to just return the
|
|
||||||
# result, or raise error
|
|
||||||
return False, df_statuses
|
|
||||||
|
|
||||||
def rpc_daily_profit(
|
def _rpc_daily_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
profit_days = {}
|
profit_days: Dict[date, Dict] = {}
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
|
||||||
|
|
||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self._freqtrade.fiat_converter
|
||||||
for day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = today - timedelta(days=day)
|
||||||
trades = Trade.query \
|
trades = Trade.query \
|
||||||
@ -134,7 +152,7 @@ class RPC(object):
|
|||||||
'trades': len(trades)
|
'trades': len(trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats = [
|
return [
|
||||||
[
|
[
|
||||||
key,
|
key,
|
||||||
'{value:.8f} {symbol}'.format(
|
'{value:.8f} {symbol}'.format(
|
||||||
@ -156,13 +174,10 @@ class RPC(object):
|
|||||||
]
|
]
|
||||||
for key, value in profit_days.items()
|
for key, value in profit_days.items()
|
||||||
]
|
]
|
||||||
return False, stats
|
|
||||||
|
|
||||||
def rpc_trade_statistics(
|
def _rpc_trade_statistics(
|
||||||
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
"""
|
""" Returns cumulative profit statistics """
|
||||||
:return: cumulative profit statistics.
|
|
||||||
"""
|
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_all_coin = []
|
profit_all_coin = []
|
||||||
@ -172,7 +187,7 @@ class RPC(object):
|
|||||||
durations = []
|
durations = []
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
current_rate = None
|
current_rate: float = 0.0
|
||||||
|
|
||||||
if not trade.open_rate:
|
if not trade.open_rate:
|
||||||
continue
|
continue
|
||||||
@ -185,7 +200,7 @@ class RPC(object):
|
|||||||
profit_closed_percent.append(profit_percent)
|
profit_closed_percent.append(profit_percent)
|
||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||||
|
|
||||||
profit_all_coin.append(
|
profit_all_coin.append(
|
||||||
@ -200,141 +215,140 @@ class RPC(object):
|
|||||||
.order_by(sql.text('profit_sum DESC')).first()
|
.order_by(sql.text('profit_sum DESC')).first()
|
||||||
|
|
||||||
if not best_pair:
|
if not best_pair:
|
||||||
return True, '*Status:* `no closed trade`'
|
raise RPCException('*Status:* `no closed trade`')
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
# FIX: we want to keep fiatconverter in a state/environment,
|
# FIX: we want to keep fiatconverter in a state/environment,
|
||||||
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self._freqtrade.fiat_converter
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||||
profit_closed_fiat = fiat.convert_amount(
|
profit_closed_fiat = fiat.convert_amount(
|
||||||
profit_closed_coin,
|
profit_closed_coin,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
)
|
||||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||||
profit_all_fiat = fiat.convert_amount(
|
profit_all_fiat = fiat.convert_amount(
|
||||||
profit_all_coin,
|
profit_all_coin,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
)
|
||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
return (
|
return {
|
||||||
False,
|
'profit_closed_coin': profit_closed_coin,
|
||||||
{
|
'profit_closed_percent': profit_closed_percent,
|
||||||
'profit_closed_coin': profit_closed_coin,
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
'profit_closed_percent': profit_closed_percent,
|
'profit_all_coin': profit_all_coin,
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
'profit_all_percent': profit_all_percent,
|
||||||
'profit_all_coin': profit_all_coin,
|
'profit_all_fiat': profit_all_fiat,
|
||||||
'profit_all_percent': profit_all_percent,
|
'trade_count': len(trades),
|
||||||
'profit_all_fiat': profit_all_fiat,
|
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
||||||
'trade_count': len(trades),
|
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
||||||
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
'best_pair': bp_pair,
|
||||||
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
'best_rate': round(bp_rate * 100, 2),
|
||||||
'best_pair': bp_pair,
|
}
|
||||||
'best_rate': round(bp_rate * 100, 2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
|
|
||||||
"""
|
|
||||||
:return: current account balance per crypto
|
|
||||||
"""
|
|
||||||
balances = [
|
|
||||||
c for c in exchange.get_balances()
|
|
||||||
if c['Balance'] or c['Available'] or c['Pending']
|
|
||||||
]
|
|
||||||
if not balances:
|
|
||||||
return True, '`All balances are zero.`'
|
|
||||||
|
|
||||||
|
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
|
||||||
|
""" Returns current account balance per crypto """
|
||||||
output = []
|
output = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
for currency in balances:
|
for coin, balance in self._freqtrade.exchange.get_balances().items():
|
||||||
coin = currency['Currency']
|
if not balance['total']:
|
||||||
|
continue
|
||||||
|
|
||||||
if coin == 'BTC':
|
if coin == 'BTC':
|
||||||
currency["Rate"] = 1.0
|
rate = 1.0
|
||||||
else:
|
else:
|
||||||
if coin == 'USDT':
|
if coin == 'USDT':
|
||||||
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
|
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
||||||
else:
|
else:
|
||||||
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
|
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||||
currency['BTC'] = currency["Rate"] * currency["Balance"]
|
est_btc: float = rate * balance['total']
|
||||||
total = total + currency['BTC']
|
total = total + est_btc
|
||||||
output.append(
|
output.append(
|
||||||
{
|
{
|
||||||
'currency': currency['Currency'],
|
'currency': coin,
|
||||||
'available': currency['Available'],
|
'available': balance['free'],
|
||||||
'balance': currency['Balance'],
|
'balance': balance['total'],
|
||||||
'pending': currency['Pending'],
|
'pending': balance['used'],
|
||||||
'est_btc': currency['BTC']
|
'est_btc': est_btc
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
fiat = self.freqtrade.fiat_converter
|
if total == 0.0:
|
||||||
|
raise RPCException('`All balances are zero.`')
|
||||||
|
|
||||||
|
fiat = self._freqtrade.fiat_converter
|
||||||
symbol = fiat_display_currency
|
symbol = fiat_display_currency
|
||||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||||
return False, (output, total, symbol, value)
|
return output, total, symbol, value
|
||||||
|
|
||||||
def rpc_start(self) -> (bool, str):
|
def _rpc_start(self) -> str:
|
||||||
"""
|
""" Handler for start """
|
||||||
Handler for start.
|
if self._freqtrade.state == State.RUNNING:
|
||||||
"""
|
return '*Status:* `already running`'
|
||||||
if self.freqtrade.state == State.RUNNING:
|
|
||||||
return True, '*Status:* `already running`'
|
|
||||||
|
|
||||||
self.freqtrade.state = State.RUNNING
|
self._freqtrade.state = State.RUNNING
|
||||||
return False, '`Starting trader ...`'
|
return '`Starting trader ...`'
|
||||||
|
|
||||||
def rpc_stop(self) -> (bool, str):
|
def _rpc_stop(self) -> str:
|
||||||
"""
|
""" Handler for stop """
|
||||||
Handler for stop.
|
if self._freqtrade.state == State.RUNNING:
|
||||||
"""
|
self._freqtrade.state = State.STOPPED
|
||||||
if self.freqtrade.state == State.RUNNING:
|
return '`Stopping trader ...`'
|
||||||
self.freqtrade.state = State.STOPPED
|
|
||||||
return False, '`Stopping trader ...`'
|
|
||||||
|
|
||||||
return True, '*Status:* `already stopped`'
|
return '*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) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
:return: error or None
|
|
||||||
"""
|
"""
|
||||||
def _exec_forcesell(trade: Trade) -> None:
|
def _exec_forcesell(trade: Trade) -> None:
|
||||||
# Check if there is there is an open order
|
# Check if there is there is an open order
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
order = exchange.get_order(trade.open_order_id)
|
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
|
|
||||||
# Cancel open LIMIT_BUY orders and close trade
|
# Cancel open LIMIT_BUY orders and close trade
|
||||||
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
|
if order and order['status'] == 'open' \
|
||||||
exchange.cancel_order(trade.open_order_id)
|
and order['type'] == 'limit' \
|
||||||
trade.close(order.get('rate') or trade.open_rate)
|
and order['side'] == 'buy':
|
||||||
# TODO: sell amount which has been bought already
|
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
return
|
trade.close(order.get('price') or trade.open_rate)
|
||||||
|
# Do the best effort, if we don't know 'filled' amount, don't try selling
|
||||||
|
if order['filled'] is None:
|
||||||
|
return
|
||||||
|
trade.amount = order['filled']
|
||||||
|
|
||||||
# Ignore trades with an attached LIMIT_SELL order
|
# Ignore trades with an attached LIMIT_SELL order
|
||||||
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
|
if order and order['status'] == 'open' \
|
||||||
|
and order['type'] == 'limit' \
|
||||||
|
and order['side'] == 'sell':
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
self.freqtrade.execute_sell(trade, current_rate)
|
self._freqtrade.execute_sell(trade, current_rate)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '`trader is not running`'
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
return False, ''
|
return
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
trade = Trade.query.filter(
|
trade = Trade.query.filter(
|
||||||
@ -345,18 +359,18 @@ class RPC(object):
|
|||||||
).first()
|
).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
logger.warning('forcesell: Invalid argument received')
|
logger.warning('forcesell: Invalid argument received')
|
||||||
return True, 'Invalid argument.'
|
raise RPCException('Invalid argument.')
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
return False, ''
|
Trade.session.flush()
|
||||||
|
|
||||||
def rpc_performance(self) -> Tuple[bool, Any]:
|
def _rpc_performance(self) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Handler for performance.
|
Handler for performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '`trader is not running`'
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
pair_rates = Trade.session.query(Trade.pair,
|
pair_rates = Trade.session.query(Trade.pair,
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
@ -365,19 +379,14 @@ class RPC(object):
|
|||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(sql.text('profit_sum DESC')) \
|
.order_by(sql.text('profit_sum DESC')) \
|
||||||
.all()
|
.all()
|
||||||
trades = []
|
return [
|
||||||
for (pair, rate, count) in pair_rates:
|
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
|
||||||
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
|
for pair, rate, count in pair_rates
|
||||||
|
]
|
||||||
|
|
||||||
return False, trades
|
def _rpc_count(self) -> List[Trade]:
|
||||||
|
""" Returns the number of trades running """
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
def rpc_count(self) -> Tuple[bool, Any]:
|
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
"""
|
|
||||||
Returns the number of trades running
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.freqtrade.state != State.RUNNING:
|
|
||||||
return True, '`trader is not running`'
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
return False, trades
|
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.rpc import RPC
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -14,36 +14,23 @@ class RPCManager(object):
|
|||||||
Class to manage RPC objects (Telegram, Slack, ...)
|
Class to manage RPC objects (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
""" Initializes all enabled rpc modules """
|
||||||
Initializes all enabled rpc modules
|
self.registered_modules: List[RPC] = []
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.freqtrade = freqtrade
|
|
||||||
|
|
||||||
self.registered_modules = []
|
# Enable telegram
|
||||||
self.telegram = None
|
if freqtrade.config['telegram'].get('enabled', False):
|
||||||
self._init()
|
|
||||||
|
|
||||||
def _init(self) -> None:
|
|
||||||
"""
|
|
||||||
Init RPC modules
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.freqtrade.config['telegram'].get('enabled', False):
|
|
||||||
logger.info('Enabling rpc.telegram ...')
|
logger.info('Enabling rpc.telegram ...')
|
||||||
self.registered_modules.append('telegram')
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.telegram = Telegram(self.freqtrade)
|
self.registered_modules.append(Telegram(freqtrade))
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""
|
""" Stops all enabled rpc modules """
|
||||||
Stops all enabled rpc modules
|
logger.info('Cleaning up rpc modules ...')
|
||||||
:return: None
|
while self.registered_modules:
|
||||||
"""
|
mod = self.registered_modules.pop()
|
||||||
if 'telegram' in self.registered_modules:
|
logger.debug('Cleaning up rpc.%s ...', mod.name)
|
||||||
logger.info('Cleaning up rpc.telegram ...')
|
mod.cleanup()
|
||||||
self.registered_modules.remove('telegram')
|
del mod
|
||||||
self.telegram.cleanup()
|
|
||||||
|
|
||||||
def send_msg(self, msg: str) -> None:
|
def send_msg(self, msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -51,6 +38,7 @@ class RPCManager(object):
|
|||||||
:param msg: message
|
:param msg: message
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logger.info(msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
if 'telegram' in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
self.telegram.send_msg(msg)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
|
mod.send_msg(msg)
|
||||||
|
@ -12,22 +12,21 @@ from telegram.error import NetworkError, TelegramError
|
|||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.debug('Included module rpc.telegram ...')
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
|
||||||
|
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
:param command_handler: Telegram CommandHandler
|
:param command_handler: Telegram CommandHandler
|
||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
"""
|
""" Decorator logic """
|
||||||
Decorator logic
|
|
||||||
"""
|
|
||||||
update = kwargs.get('update') or args[1]
|
update = kwargs.get('update') or args[1]
|
||||||
|
|
||||||
# Reject unauthorized messages
|
# Reject unauthorized messages
|
||||||
@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
|||||||
|
|
||||||
|
|
||||||
class Telegram(RPC):
|
class Telegram(RPC):
|
||||||
"""
|
""" This class handles all telegram communication """
|
||||||
Telegram, this class send messages to Telegram
|
|
||||||
"""
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "telegram"
|
||||||
|
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
"""
|
||||||
Init the Telegram call, and init the super class RPC
|
Init the Telegram call, and init the super class RPC
|
||||||
@ -65,7 +67,7 @@ class Telegram(RPC):
|
|||||||
"""
|
"""
|
||||||
super().__init__(freqtrade)
|
super().__init__(freqtrade)
|
||||||
|
|
||||||
self._updater = None
|
self._updater: Updater = None
|
||||||
self._config = freqtrade.config
|
self._config = freqtrade.config
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
@ -74,12 +76,7 @@ class Telegram(RPC):
|
|||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
and starts polling for message updates
|
and starts polling for message updates
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||||
|
|
||||||
# Register command handler and start telegram message polling
|
# Register command handler and start telegram message polling
|
||||||
@ -93,6 +90,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),
|
||||||
]
|
]
|
||||||
@ -114,16 +112,11 @@ class Telegram(RPC):
|
|||||||
Stops all running telegram threads.
|
Stops all running telegram threads.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._updater.stop()
|
self._updater.stop()
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def send_msg(self, msg: str) -> None:
|
||||||
"""
|
""" Send a message to telegram channel """
|
||||||
Returns True if the telegram module is activated, False otherwise
|
self._send_msg(msg)
|
||||||
"""
|
|
||||||
return bool(self._config.get('telegram', {}).get('enabled', False))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(self, bot: Bot, update: Update) -> None:
|
def _status(self, bot: Bot, update: Update) -> None:
|
||||||
@ -142,13 +135,11 @@ class Telegram(RPC):
|
|||||||
self._status_table(bot, update)
|
self._status_table(bot, update)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch open trade
|
try:
|
||||||
(error, trades) = self.rpc_trade_status()
|
for trade_msg in self._rpc_trade_status():
|
||||||
if error:
|
self._send_msg(trade_msg, bot=bot)
|
||||||
self.send_msg(trades, bot=bot)
|
except RPCException as e:
|
||||||
else:
|
self._send_msg(str(e), bot=bot)
|
||||||
for trademsg in trades:
|
|
||||||
self.send_msg(trademsg, bot=bot)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status_table(self, bot: Bot, update: Update) -> None:
|
def _status_table(self, bot: Bot, update: Update) -> None:
|
||||||
@ -159,15 +150,12 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Fetch open trade
|
try:
|
||||||
(err, df_statuses) = self.rpc_status_table()
|
df_statuses = self._rpc_status_table()
|
||||||
if err:
|
|
||||||
self.send_msg(df_statuses, bot=bot)
|
|
||||||
else:
|
|
||||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||||
message = "<pre>{}</pre>".format(message)
|
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
|
||||||
|
except RPCException as e:
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _daily(self, bot: Bot, update: Update) -> None:
|
def _daily(self, bot: Bot, update: Update) -> None:
|
||||||
@ -182,14 +170,12 @@ class Telegram(RPC):
|
|||||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
timescale = 7
|
timescale = 7
|
||||||
(error, stats) = self.rpc_daily_profit(
|
try:
|
||||||
timescale,
|
stats = self._rpc_daily_profit(
|
||||||
self._config['stake_currency'],
|
timescale,
|
||||||
self._config['fiat_display_currency']
|
self._config['stake_currency'],
|
||||||
)
|
self._config['fiat_display_currency']
|
||||||
if error:
|
)
|
||||||
self.send_msg(stats, bot=bot)
|
|
||||||
else:
|
|
||||||
stats = tabulate(stats,
|
stats = tabulate(stats,
|
||||||
headers=[
|
headers=[
|
||||||
'Day',
|
'Day',
|
||||||
@ -198,11 +184,10 @@ class Telegram(RPC):
|
|||||||
],
|
],
|
||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
||||||
.format(
|
.format(timescale, stats)
|
||||||
timescale,
|
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
stats
|
except RPCException as e:
|
||||||
)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(self, bot: Bot, update: Update) -> None:
|
def _profit(self, bot: Bot, update: Update) -> None:
|
||||||
@ -213,69 +198,63 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, stats) = self.rpc_trade_statistics(
|
try:
|
||||||
self._config['stake_currency'],
|
stats = self._rpc_trade_statistics(
|
||||||
self._config['fiat_display_currency']
|
self._config['stake_currency'],
|
||||||
)
|
self._config['fiat_display_currency'])
|
||||||
if error:
|
|
||||||
self.send_msg(stats, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Message to display
|
# Message to display
|
||||||
markdown_msg = "*ROI:* Close trades\n" \
|
markdown_msg = "*ROI:* Close trades\n" \
|
||||||
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
||||||
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
||||||
"*ROI:* All trades\n" \
|
"*ROI:* All trades\n" \
|
||||||
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
||||||
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
||||||
"*Total Trade Count:* `{trade_count}`\n" \
|
"*Total Trade Count:* `{trade_count}`\n" \
|
||||||
"*First Trade opened:* `{first_trade_date}`\n" \
|
"*First Trade opened:* `{first_trade_date}`\n" \
|
||||||
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||||
"*Avg. Duration:* `{avg_duration}`\n" \
|
"*Avg. Duration:* `{avg_duration}`\n" \
|
||||||
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
||||||
.format(
|
.format(
|
||||||
coin=self._config['stake_currency'],
|
coin=self._config['stake_currency'],
|
||||||
fiat=self._config['fiat_display_currency'],
|
fiat=self._config['fiat_display_currency'],
|
||||||
profit_closed_coin=stats['profit_closed_coin'],
|
profit_closed_coin=stats['profit_closed_coin'],
|
||||||
profit_closed_percent=stats['profit_closed_percent'],
|
profit_closed_percent=stats['profit_closed_percent'],
|
||||||
profit_closed_fiat=stats['profit_closed_fiat'],
|
profit_closed_fiat=stats['profit_closed_fiat'],
|
||||||
profit_all_coin=stats['profit_all_coin'],
|
profit_all_coin=stats['profit_all_coin'],
|
||||||
profit_all_percent=stats['profit_all_percent'],
|
profit_all_percent=stats['profit_all_percent'],
|
||||||
profit_all_fiat=stats['profit_all_fiat'],
|
profit_all_fiat=stats['profit_all_fiat'],
|
||||||
trade_count=stats['trade_count'],
|
trade_count=stats['trade_count'],
|
||||||
first_trade_date=stats['first_trade_date'],
|
first_trade_date=stats['first_trade_date'],
|
||||||
latest_trade_date=stats['latest_trade_date'],
|
latest_trade_date=stats['latest_trade_date'],
|
||||||
avg_duration=stats['avg_duration'],
|
avg_duration=stats['avg_duration'],
|
||||||
best_pair=stats['best_pair'],
|
best_pair=stats['best_pair'],
|
||||||
best_rate=stats['best_rate']
|
best_rate=stats['best_rate']
|
||||||
)
|
)
|
||||||
self.send_msg(markdown_msg, bot=bot)
|
self._send_msg(markdown_msg, bot=bot)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _balance(self, bot: Bot, update: Update) -> None:
|
def _balance(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
""" Handler for /balance """
|
||||||
Handler for /balance
|
try:
|
||||||
"""
|
currencys, total, symbol, value = \
|
||||||
(error, result) = self.rpc_balance(self._config['fiat_display_currency'])
|
self._rpc_balance(self._config['fiat_display_currency'])
|
||||||
if error:
|
output = ''
|
||||||
self.send_msg('`All balances are zero.`')
|
for currency in currencys:
|
||||||
return
|
output += "*{currency}:*\n" \
|
||||||
|
"\t`Available: {available: .8f}`\n" \
|
||||||
|
"\t`Balance: {balance: .8f}`\n" \
|
||||||
|
"\t`Pending: {pending: .8f}`\n" \
|
||||||
|
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||||
|
|
||||||
(currencys, total, symbol, value) = result
|
output += "\n*Estimated Value*:\n" \
|
||||||
output = ''
|
"\t`BTC: {0: .8f}`\n" \
|
||||||
for currency in currencys:
|
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
|
||||||
output += """*Currency*: {currency}
|
self._send_msg(output, bot=bot)
|
||||||
*Available*: {available}
|
except RPCException as e:
|
||||||
*Balance*: {balance}
|
self._send_msg(str(e), bot=bot)
|
||||||
*Pending*: {pending}
|
|
||||||
*Est. BTC*: {est_btc: .8f}
|
|
||||||
""".format(**currency)
|
|
||||||
|
|
||||||
output += """*Estimated Value*:
|
|
||||||
*BTC*: {0: .8f}
|
|
||||||
*{1}*: {2: .2f}
|
|
||||||
""".format(total, symbol, value)
|
|
||||||
self.send_msg(output)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _start(self, bot: Bot, update: Update) -> None:
|
def _start(self, bot: Bot, update: Update) -> None:
|
||||||
@ -286,9 +265,8 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, msg) = self.rpc_start()
|
msg = self._rpc_start()
|
||||||
if error:
|
self._send_msg(msg, bot=bot)
|
||||||
self.send_msg(msg, bot=bot)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stop(self, bot: Bot, update: Update) -> None:
|
def _stop(self, bot: Bot, update: Update) -> None:
|
||||||
@ -299,8 +277,20 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, msg) = self.rpc_stop()
|
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:
|
||||||
@ -313,10 +303,10 @@ class Telegram(RPC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||||
(error, message) = self.rpc_forcesell(trade_id)
|
try:
|
||||||
if error:
|
self._rpc_forcesell(trade_id)
|
||||||
self.send_msg(message, bot=bot)
|
except RPCException as e:
|
||||||
return
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(self, bot: Bot, update: Update) -> None:
|
def _performance(self, bot: Bot, update: Update) -> None:
|
||||||
@ -327,19 +317,18 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, trades) = self.rpc_performance()
|
try:
|
||||||
if error:
|
trades = self._rpc_performance()
|
||||||
self.send_msg(trades, bot=bot)
|
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
||||||
return
|
index=i + 1,
|
||||||
|
pair=trade['pair'],
|
||||||
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
profit=trade['profit'],
|
||||||
index=i + 1,
|
count=trade['count']
|
||||||
pair=trade['pair'],
|
) for i, trade in enumerate(trades))
|
||||||
profit=trade['profit'],
|
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||||
count=trade['count']
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
) for i, trade in enumerate(trades))
|
except RPCException as e:
|
||||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _count(self, bot: Bot, update: Update) -> None:
|
def _count(self, bot: Bot, update: Update) -> None:
|
||||||
@ -350,19 +339,18 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, trades) = self.rpc_count()
|
try:
|
||||||
if error:
|
trades = self._rpc_count()
|
||||||
self.send_msg(trades, bot=bot)
|
message = tabulate({
|
||||||
return
|
'current': [len(trades)],
|
||||||
|
'max': [self._config['max_open_trades']],
|
||||||
message = tabulate({
|
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
|
||||||
'current': [len(trades)],
|
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
|
||||||
'max': [self._config['max_open_trades']],
|
message = "<pre>{}</pre>".format(message)
|
||||||
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
|
logger.debug(message)
|
||||||
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
message = "<pre>{}</pre>".format(message)
|
except RPCException as e:
|
||||||
logger.debug(message)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _help(self, bot: Bot, update: Update) -> None:
|
def _help(self, bot: Bot, update: Update) -> None:
|
||||||
@ -388,7 +376,7 @@ class Telegram(RPC):
|
|||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n" \
|
||||||
"*/version:* `Show version`"
|
"*/version:* `Show version`"
|
||||||
|
|
||||||
self.send_msg(message, bot=bot)
|
self._send_msg(message, bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _version(self, bot: Bot, update: Update) -> None:
|
def _version(self, bot: Bot, update: Update) -> None:
|
||||||
@ -399,10 +387,10 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||||
|
|
||||||
def send_msg(self, msg: str, bot: Bot = None,
|
def _send_msg(self, msg: str, bot: Bot = None,
|
||||||
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
:param msg: message
|
:param msg: message
|
||||||
@ -410,9 +398,6 @@ class Telegram(RPC):
|
|||||||
:param parse_mode: telegram parse mode
|
:param parse_mode: telegram parse mode
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
bot = bot or self._updater.bot
|
bot = bot or self._updater.bot
|
||||||
|
|
||||||
keyboard = [['/daily', '/profit', '/balance'],
|
keyboard = [['/daily', '/profit', '/balance'],
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def import_strategy(strategy: IStrategy) -> IStrategy:
|
||||||
|
"""
|
||||||
|
Imports given Strategy instance to global scope
|
||||||
|
of freqtrade.strategy and returns an instance of it
|
||||||
|
"""
|
||||||
|
# Copy all attributes from base class and class
|
||||||
|
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
|
||||||
|
# Adjust module name
|
||||||
|
attr['__module__'] = 'freqtrade.strategy'
|
||||||
|
|
||||||
|
name = strategy.__class__.__name__
|
||||||
|
clazz = type(name, (IStrategy,), attr)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'Imported strategy %s.%s as %s.%s',
|
||||||
|
strategy.__module__, strategy.__class__.__name__,
|
||||||
|
clazz.__module__, strategy.__class__.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Modify global scope to declare class
|
||||||
|
globals()[name] = clazz
|
||||||
|
|
||||||
|
return clazz()
|
@ -26,7 +26,7 @@ class DefaultStrategy(IStrategy):
|
|||||||
stoploss = -0.10
|
stoploss = -0.10
|
||||||
|
|
||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = 5
|
ticker_interval = '5m'
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
IStrategy interface
|
IStrategy interface
|
||||||
This module defines the interface to apply for strategies
|
This module defines the interface to apply for strategies
|
||||||
"""
|
"""
|
||||||
|
from typing import Dict
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -16,9 +16,13 @@ class IStrategy(ABC):
|
|||||||
Attributes you can use:
|
Attributes you can use:
|
||||||
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
stoploss -> float: optimal stoploss designed for the strategy
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
ticker_interval -> int: value of the ticker interval to use for the strategy
|
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
minimal_roi: Dict
|
||||||
|
stoploss: float
|
||||||
|
ticker_interval: str
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
|
@ -11,6 +11,7 @@ from collections import OrderedDict
|
|||||||
from typing import Optional, Dict, Type
|
from typing import Optional, Dict, Type
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +34,8 @@ class StrategyResolver(object):
|
|||||||
|
|
||||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||||
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
|
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
||||||
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
@ -59,10 +61,9 @@ class StrategyResolver(object):
|
|||||||
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
||||||
key=lambda t: t[0]))
|
key=lambda t: t[0]))
|
||||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
self.strategy.ticker_interval = int(self.strategy.ticker_interval)
|
|
||||||
|
|
||||||
def _load_strategy(
|
def _load_strategy(
|
||||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
|
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified strategy.
|
Search and loads the specified strategy.
|
||||||
:param strategy_name: name of the module to import
|
:param strategy_name: name of the module to import
|
||||||
@ -71,7 +72,7 @@ class StrategyResolver(object):
|
|||||||
"""
|
"""
|
||||||
current_path = os.path.dirname(os.path.realpath(__file__))
|
current_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
|
os.path.join(os.getcwd(), 'user_data', 'strategies'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -80,10 +81,13 @@ class StrategyResolver(object):
|
|||||||
abs_paths.insert(0, extra_dir)
|
abs_paths.insert(0, extra_dir)
|
||||||
|
|
||||||
for path in abs_paths:
|
for path in abs_paths:
|
||||||
strategy = self._search_strategy(path, strategy_name)
|
try:
|
||||||
if strategy:
|
strategy = self._search_strategy(path, strategy_name)
|
||||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
if strategy:
|
||||||
return strategy
|
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
||||||
|
return import_strategy(strategy)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning('Path "%s" does not exist', path)
|
||||||
|
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Impossible to load Strategy '{}'. This class does not exist"
|
"Impossible to load Strategy '{}'. This class does not exist"
|
||||||
@ -100,9 +104,9 @@ class StrategyResolver(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Generate spec based on absolute path
|
# Generate spec based on absolute path
|
||||||
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
spec = importlib.util.spec_from_file_location('unknown', module_path)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||||
|
|
||||||
valid_strategies_gen = (
|
valid_strategies_gen = (
|
||||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||||
|
@ -2,17 +2,18 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Dict, Optional
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
logging.getLogger('').setLevel(logging.INFO)
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
@ -26,6 +27,20 @@ def log_has(line, logs):
|
|||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_exchange(mocker, api_mock=None) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
if api_mock:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
else:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||||
|
|
||||||
|
|
||||||
|
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
|
||||||
|
patch_exchange(mocker, api_mock)
|
||||||
|
exchange = Exchange(config)
|
||||||
|
return exchange
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
"""
|
"""
|
||||||
@ -34,16 +49,38 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|||||||
:param config: Config to pass to the bot
|
:param config: Config to pass to the bot
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
||||||
|
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
patch_exchange(mocker, None)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||||
|
|
||||||
return FreqtradeBot(config, create_engine('sqlite://'))
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Mocker to coinmarketcap to speed up tests
|
||||||
|
:param mocker: mocker to patch coinmarketcap class
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
tickermock = MagicMock(return_value={'price_usd': 12345.0})
|
||||||
|
listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC',
|
||||||
|
'website_slug': 'bitcoin'},
|
||||||
|
{'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH',
|
||||||
|
'website_slug': 'ethereum'}
|
||||||
|
]})
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
ticker=tickermock,
|
||||||
|
listings=listmock,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -54,7 +91,7 @@ def default_conf():
|
|||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.001,
|
"stake_amount": 0.001,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval": 5,
|
"ticker_interval": '5m',
|
||||||
"dry_run": True,
|
"dry_run": True,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
@ -73,11 +110,10 @@ def default_conf():
|
|||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC_ETH",
|
"ETH/BTC",
|
||||||
"BTC_TKN",
|
"LTC/BTC",
|
||||||
"BTC_TRST",
|
"XRP/BTC",
|
||||||
"BTC_SWT",
|
"NEO/BTC"
|
||||||
"BTC_BCC"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
@ -86,7 +122,8 @@ def default_conf():
|
|||||||
"chat_id": "0"
|
"chat_id": "0"
|
||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"loglevel": logging.DEBUG
|
"db_url": "sqlite://",
|
||||||
|
"loglevel": logging.DEBUG,
|
||||||
}
|
}
|
||||||
validate(configuration, constants.CONF_SCHEMA)
|
validate(configuration, constants.CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
@ -99,6 +136,11 @@ def update():
|
|||||||
return _update
|
return _update
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fee():
|
||||||
|
return MagicMock(return_value=0.0025)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker():
|
def ticker():
|
||||||
return MagicMock(return_value={
|
return MagicMock(return_value={
|
||||||
@ -127,46 +169,178 @@ def ticker_sell_down():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def health():
|
def markets():
|
||||||
return MagicMock(return_value=[{
|
return MagicMock(return_value=[
|
||||||
'Currency': 'BTC',
|
{
|
||||||
'IsActive': True,
|
'id': 'ethbtc',
|
||||||
'LastChecked': '2017-11-13T20:15:00.00',
|
'symbol': 'ETH/BTC',
|
||||||
'Notice': None
|
'base': 'ETH',
|
||||||
}, {
|
'quote': 'BTC',
|
||||||
'Currency': 'ETH',
|
'active': True,
|
||||||
'IsActive': True,
|
'precision': {
|
||||||
'LastChecked': '2017-11-13T20:15:00.00',
|
'price': 8,
|
||||||
'Notice': None
|
'amount': 8,
|
||||||
}, {
|
'cost': 8,
|
||||||
'Currency': 'TRST',
|
},
|
||||||
'IsActive': True,
|
'lot': 0.00000001,
|
||||||
'LastChecked': '2017-11-13T20:15:00.00',
|
'limits': {
|
||||||
'Notice': None
|
'amount': {
|
||||||
}, {
|
'min': 0.01,
|
||||||
'Currency': 'SWT',
|
'max': 1000,
|
||||||
'IsActive': True,
|
},
|
||||||
'LastChecked': '2017-11-13T20:15:00.00',
|
'price': 500000,
|
||||||
'Notice': None
|
'cost': {
|
||||||
}, {
|
'min': 1,
|
||||||
'Currency': 'BCC',
|
'max': 500000,
|
||||||
'IsActive': False,
|
},
|
||||||
'LastChecked': '2017-11-13T20:15:00.00',
|
},
|
||||||
'Notice': None
|
'info': '',
|
||||||
}])
|
},
|
||||||
|
{
|
||||||
|
'id': 'tknbtc',
|
||||||
|
'symbol': 'TKN/BTC',
|
||||||
|
'base': 'TKN',
|
||||||
|
'quote': 'BTC',
|
||||||
|
'active': True,
|
||||||
|
'precision': {
|
||||||
|
'price': 8,
|
||||||
|
'amount': 8,
|
||||||
|
'cost': 8,
|
||||||
|
},
|
||||||
|
'lot': 0.00000001,
|
||||||
|
'limits': {
|
||||||
|
'amount': {
|
||||||
|
'min': 0.01,
|
||||||
|
'max': 1000,
|
||||||
|
},
|
||||||
|
'price': 500000,
|
||||||
|
'cost': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'info': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'blkbtc',
|
||||||
|
'symbol': 'BLK/BTC',
|
||||||
|
'base': 'BLK',
|
||||||
|
'quote': 'BTC',
|
||||||
|
'active': True,
|
||||||
|
'precision': {
|
||||||
|
'price': 8,
|
||||||
|
'amount': 8,
|
||||||
|
'cost': 8,
|
||||||
|
},
|
||||||
|
'lot': 0.00000001,
|
||||||
|
'limits': {
|
||||||
|
'amount': {
|
||||||
|
'min': 0.01,
|
||||||
|
'max': 1000,
|
||||||
|
},
|
||||||
|
'price': 500000,
|
||||||
|
'cost': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'info': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'ltcbtc',
|
||||||
|
'symbol': 'LTC/BTC',
|
||||||
|
'base': 'LTC',
|
||||||
|
'quote': 'BTC',
|
||||||
|
'active': False,
|
||||||
|
'precision': {
|
||||||
|
'price': 8,
|
||||||
|
'amount': 8,
|
||||||
|
'cost': 8,
|
||||||
|
},
|
||||||
|
'lot': 0.00000001,
|
||||||
|
'limits': {
|
||||||
|
'amount': {
|
||||||
|
'min': 0.01,
|
||||||
|
'max': 1000,
|
||||||
|
},
|
||||||
|
'price': 500000,
|
||||||
|
'cost': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'info': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'xrpbtc',
|
||||||
|
'symbol': 'XRP/BTC',
|
||||||
|
'base': 'XRP',
|
||||||
|
'quote': 'BTC',
|
||||||
|
'active': False,
|
||||||
|
'precision': {
|
||||||
|
'price': 8,
|
||||||
|
'amount': 8,
|
||||||
|
'cost': 8,
|
||||||
|
},
|
||||||
|
'lot': 0.00000001,
|
||||||
|
'limits': {
|
||||||
|
'amount': {
|
||||||
|
'min': 0.01,
|
||||||
|
'max': 1000,
|
||||||
|
},
|
||||||
|
'price': 500000,
|
||||||
|
'cost': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'info': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'neobtc',
|
||||||
|
'symbol': 'NEO/BTC',
|
||||||
|
'base': 'NEO',
|
||||||
|
'quote': 'BTC',
|
||||||
|
'active': False,
|
||||||
|
'precision': {
|
||||||
|
'price': 8,
|
||||||
|
'amount': 8,
|
||||||
|
'cost': 8,
|
||||||
|
},
|
||||||
|
'lot': 0.00000001,
|
||||||
|
'limits': {
|
||||||
|
'amount': {
|
||||||
|
'min': 0.01,
|
||||||
|
'max': 1000,
|
||||||
|
},
|
||||||
|
'price': 500000,
|
||||||
|
'cost': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 500000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'info': '',
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
def markets_empty():
|
||||||
|
return MagicMock(return_value=[])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
def limit_buy_order():
|
def limit_buy_order():
|
||||||
return {
|
return {
|
||||||
'id': 'mocked_limit_buy',
|
'id': 'mocked_limit_buy',
|
||||||
'type': 'LIMIT_BUY',
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
'pair': 'mocked',
|
'pair': 'mocked',
|
||||||
'opened': str(arrow.utcnow().datetime),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'rate': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'closed': str(arrow.utcnow().datetime),
|
'status': 'closed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -174,12 +348,14 @@ def limit_buy_order():
|
|||||||
def limit_buy_order_old():
|
def limit_buy_order_old():
|
||||||
return {
|
return {
|
||||||
'id': 'mocked_limit_buy_old',
|
'id': 'mocked_limit_buy_old',
|
||||||
'type': 'LIMIT_BUY',
|
'type': 'limit',
|
||||||
'pair': 'BTC_ETH',
|
'side': 'buy',
|
||||||
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'pair': 'mocked',
|
||||||
'rate': 0.00001099,
|
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
||||||
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
|
'status': 'open'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -187,12 +363,14 @@ def limit_buy_order_old():
|
|||||||
def limit_sell_order_old():
|
def limit_sell_order_old():
|
||||||
return {
|
return {
|
||||||
'id': 'mocked_limit_sell_old',
|
'id': 'mocked_limit_sell_old',
|
||||||
'type': 'LIMIT_SELL',
|
'type': 'limit',
|
||||||
'pair': 'BTC_ETH',
|
'side': 'sell',
|
||||||
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'pair': 'ETH/BTC',
|
||||||
'rate': 0.00001099,
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
|
'status': 'open'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -200,12 +378,14 @@ def limit_sell_order_old():
|
|||||||
def limit_buy_order_old_partial():
|
def limit_buy_order_old_partial():
|
||||||
return {
|
return {
|
||||||
'id': 'mocked_limit_buy_old_partial',
|
'id': 'mocked_limit_buy_old_partial',
|
||||||
'type': 'LIMIT_BUY',
|
'type': 'limit',
|
||||||
'pair': 'BTC_ETH',
|
'side': 'buy',
|
||||||
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
|
'pair': 'ETH/BTC',
|
||||||
'rate': 0.00001099,
|
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
|
||||||
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 67.99181073,
|
'remaining': 67.99181073,
|
||||||
|
'status': 'open'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -213,86 +393,228 @@ def limit_buy_order_old_partial():
|
|||||||
def limit_sell_order():
|
def limit_sell_order():
|
||||||
return {
|
return {
|
||||||
'id': 'mocked_limit_sell',
|
'id': 'mocked_limit_sell',
|
||||||
'type': 'LIMIT_SELL',
|
'type': 'limit',
|
||||||
|
'side': 'sell',
|
||||||
'pair': 'mocked',
|
'pair': 'mocked',
|
||||||
'opened': str(arrow.utcnow().datetime),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
'rate': 0.00001173,
|
'price': 0.00001173,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'closed': str(arrow.utcnow().datetime),
|
'status': 'closed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history():
|
def ticker_history():
|
||||||
return [
|
return [
|
||||||
{
|
[
|
||||||
"O": 8.794e-05,
|
1511686200000, # unix timestamp ms
|
||||||
"H": 8.948e-05,
|
8.794e-05, # open
|
||||||
"L": 8.794e-05,
|
8.948e-05, # high
|
||||||
"C": 8.88e-05,
|
8.794e-05, # low
|
||||||
"V": 991.09056638,
|
8.88e-05, # close
|
||||||
"T": "2017-11-26T08:50:00",
|
0.0877869, # volume (in quote currency)
|
||||||
"BV": 0.0877869
|
],
|
||||||
},
|
[
|
||||||
{
|
1511686500000,
|
||||||
"O": 8.88e-05,
|
8.88e-05,
|
||||||
"H": 8.942e-05,
|
8.942e-05,
|
||||||
"L": 8.88e-05,
|
8.88e-05,
|
||||||
"C": 8.893e-05,
|
8.893e-05,
|
||||||
"V": 658.77935965,
|
0.05874751,
|
||||||
"T": "2017-11-26T08:55:00",
|
],
|
||||||
"BV": 0.05874751
|
[
|
||||||
},
|
1511686800000,
|
||||||
{
|
8.891e-05,
|
||||||
"O": 8.891e-05,
|
8.893e-05,
|
||||||
"H": 8.893e-05,
|
8.875e-05,
|
||||||
"L": 8.875e-05,
|
8.877e-05,
|
||||||
"C": 8.877e-05,
|
0.7039405
|
||||||
"V": 7920.73570705,
|
]
|
||||||
"T": "2017-11-26T09:00:00",
|
|
||||||
"BV": 0.7039405
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history_without_bv():
|
def tickers():
|
||||||
return [
|
return MagicMock(return_value={
|
||||||
{
|
'ETH/BTC': {
|
||||||
"O": 8.794e-05,
|
'symbol': 'ETH/BTC',
|
||||||
"H": 8.948e-05,
|
'timestamp': 1522014806207,
|
||||||
"L": 8.794e-05,
|
'datetime': '2018-03-25T21:53:26.207Z',
|
||||||
"C": 8.88e-05,
|
'high': 0.061697,
|
||||||
"V": 991.09056638,
|
'low': 0.060531,
|
||||||
"T": "2017-11-26T08:50:00"
|
'bid': 0.061588,
|
||||||
|
'bidVolume': 3.321,
|
||||||
|
'ask': 0.061655,
|
||||||
|
'askVolume': 0.212,
|
||||||
|
'vwap': 0.06105296,
|
||||||
|
'open': 0.060809,
|
||||||
|
'close': 0.060761,
|
||||||
|
'first': None,
|
||||||
|
'last': 0.061588,
|
||||||
|
'change': 1.281,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 111649.001,
|
||||||
|
'quoteVolume': 6816.50176926,
|
||||||
|
'info': {}
|
||||||
},
|
},
|
||||||
{
|
'TKN/BTC': {
|
||||||
"O": 8.88e-05,
|
'symbol': 'TKN/BTC',
|
||||||
"H": 8.942e-05,
|
'timestamp': 1522014806169,
|
||||||
"L": 8.88e-05,
|
'datetime': '2018-03-25T21:53:26.169Z',
|
||||||
"C": 8.893e-05,
|
'high': 0.01885,
|
||||||
"V": 658.77935965,
|
'low': 0.018497,
|
||||||
"T": "2017-11-26T08:55:00"
|
'bid': 0.018799,
|
||||||
|
'bidVolume': 8.38,
|
||||||
|
'ask': 0.018802,
|
||||||
|
'askVolume': 15.0,
|
||||||
|
'vwap': 0.01869197,
|
||||||
|
'open': 0.018585,
|
||||||
|
'close': 0.018573,
|
||||||
|
'baseVolume': 81058.66,
|
||||||
|
'quoteVolume': 2247.48374509,
|
||||||
},
|
},
|
||||||
{
|
'BLK/BTC': {
|
||||||
"O": 8.891e-05,
|
'symbol': 'BLK/BTC',
|
||||||
"H": 8.893e-05,
|
'timestamp': 1522014806072,
|
||||||
"L": 8.875e-05,
|
'datetime': '2018-03-25T21:53:26.720Z',
|
||||||
"C": 8.877e-05,
|
'high': 0.007745,
|
||||||
"V": 7920.73570705,
|
'low': 0.007512,
|
||||||
"T": "2017-11-26T09:00:00"
|
'bid': 0.007729,
|
||||||
|
'bidVolume': 0.01,
|
||||||
|
'ask': 0.007743,
|
||||||
|
'askVolume': 21.37,
|
||||||
|
'vwap': 0.00761466,
|
||||||
|
'open': 0.007653,
|
||||||
|
'close': 0.007652,
|
||||||
|
'first': None,
|
||||||
|
'last': 0.007743,
|
||||||
|
'change': 1.176,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 295152.26,
|
||||||
|
'quoteVolume': 1515.14631229,
|
||||||
|
'info': {}
|
||||||
|
},
|
||||||
|
'LTC/BTC': {
|
||||||
|
'symbol': 'LTC/BTC',
|
||||||
|
'timestamp': 1523787258992,
|
||||||
|
'datetime': '2018-04-15T10:14:19.992Z',
|
||||||
|
'high': 0.015978,
|
||||||
|
'low': 0.0157,
|
||||||
|
'bid': 0.015954,
|
||||||
|
'bidVolume': 12.83,
|
||||||
|
'ask': 0.015957,
|
||||||
|
'askVolume': 0.49,
|
||||||
|
'vwap': 0.01581636,
|
||||||
|
'open': 0.015823,
|
||||||
|
'close': 0.01582,
|
||||||
|
'first': None,
|
||||||
|
'last': 0.015951,
|
||||||
|
'change': 0.809,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 88620.68,
|
||||||
|
'quoteVolume': 1401.65697943,
|
||||||
|
'info': {}
|
||||||
|
},
|
||||||
|
'ETH/USDT': {
|
||||||
|
'symbol': 'ETH/USDT',
|
||||||
|
'timestamp': 1522014804118,
|
||||||
|
'datetime': '2018-03-25T21:53:24.118Z',
|
||||||
|
'high': 530.88,
|
||||||
|
'low': 512.0,
|
||||||
|
'bid': 529.73,
|
||||||
|
'bidVolume': 0.2,
|
||||||
|
'ask': 530.21,
|
||||||
|
'askVolume': 0.2464,
|
||||||
|
'vwap': 521.02438405,
|
||||||
|
'open': 527.27,
|
||||||
|
'close': 528.42,
|
||||||
|
'first': None,
|
||||||
|
'last': 530.21,
|
||||||
|
'change': 0.558,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 72300.0659,
|
||||||
|
'quoteVolume': 37670097.3022171,
|
||||||
|
'info': {}
|
||||||
|
},
|
||||||
|
'TKN/USDT': {
|
||||||
|
'symbol': 'TKN/USDT',
|
||||||
|
'timestamp': 1522014806198,
|
||||||
|
'datetime': '2018-03-25T21:53:26.198Z',
|
||||||
|
'high': 8718.0,
|
||||||
|
'low': 8365.77,
|
||||||
|
'bid': 8603.64,
|
||||||
|
'bidVolume': 0.15846,
|
||||||
|
'ask': 8603.67,
|
||||||
|
'askVolume': 0.069147,
|
||||||
|
'vwap': 8536.35621697,
|
||||||
|
'open': 8680.0,
|
||||||
|
'close': 8680.0,
|
||||||
|
'first': None,
|
||||||
|
'last': 8603.67,
|
||||||
|
'change': -0.879,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 30414.604298,
|
||||||
|
'quoteVolume': 259629896.48584127,
|
||||||
|
'info': {}
|
||||||
|
},
|
||||||
|
'BLK/USDT': {
|
||||||
|
'symbol': 'BLK/USDT',
|
||||||
|
'timestamp': 1522014806145,
|
||||||
|
'datetime': '2018-03-25T21:53:26.145Z',
|
||||||
|
'high': 66.95,
|
||||||
|
'low': 63.38,
|
||||||
|
'bid': 66.473,
|
||||||
|
'bidVolume': 4.968,
|
||||||
|
'ask': 66.54,
|
||||||
|
'askVolume': 2.704,
|
||||||
|
'vwap': 65.0526901,
|
||||||
|
'open': 66.43,
|
||||||
|
'close': 66.383,
|
||||||
|
'first': None,
|
||||||
|
'last': 66.5,
|
||||||
|
'change': 0.105,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 294106.204,
|
||||||
|
'quoteVolume': 19132399.743954,
|
||||||
|
'info': {}
|
||||||
|
},
|
||||||
|
'LTC/USDT': {
|
||||||
|
'symbol': 'LTC/USDT',
|
||||||
|
'timestamp': 1523787257812,
|
||||||
|
'datetime': '2018-04-15T10:14:18.812Z',
|
||||||
|
'high': 129.94,
|
||||||
|
'low': 124.0,
|
||||||
|
'bid': 129.28,
|
||||||
|
'bidVolume': 0.03201,
|
||||||
|
'ask': 129.52,
|
||||||
|
'askVolume': 0.14529,
|
||||||
|
'vwap': 126.92838682,
|
||||||
|
'open': 127.0,
|
||||||
|
'close': 127.1,
|
||||||
|
'first': None,
|
||||||
|
'last': 129.28,
|
||||||
|
'change': 1.795,
|
||||||
|
'percentage': None,
|
||||||
|
'average': None,
|
||||||
|
'baseVolume': 59698.79897,
|
||||||
|
'quoteVolume': 29132399.743954,
|
||||||
|
'info': {}
|
||||||
}
|
}
|
||||||
]
|
})
|
||||||
|
|
||||||
|
|
||||||
# FIX: Perhaps change result fixture to use BTC_UNITEST instead?
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
||||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
# Create an fixture/function
|
# Create an fixture/function
|
||||||
# that inserts a trade of some type and open-status
|
# that inserts a trade of some type and open-status
|
||||||
@ -300,132 +622,88 @@ def result():
|
|||||||
# See tests in rpc/main that could use this
|
# See tests in rpc/main that could use this
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def trades_for_order():
|
||||||
|
return [{'info': {'id': 34567,
|
||||||
|
'orderId': 123456,
|
||||||
|
'price': '0.24544100',
|
||||||
|
'qty': '8.00000000',
|
||||||
|
'commission': '0.00800000',
|
||||||
|
'commissionAsset': 'LTC',
|
||||||
|
'time': 1521663363189,
|
||||||
|
'isBuyer': True,
|
||||||
|
'isMaker': False,
|
||||||
|
'isBestMatch': True},
|
||||||
|
'timestamp': 1521663363189,
|
||||||
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
|
'symbol': 'LTC/ETH',
|
||||||
|
'id': '34567',
|
||||||
|
'order': '123456',
|
||||||
|
'type': None,
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 0.245441,
|
||||||
|
'cost': 1.963528,
|
||||||
|
'amount': 8.0,
|
||||||
|
'fee': {'cost': 0.008, 'currency': 'LTC'}}]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def trades_for_order2():
|
||||||
|
return [{'info': {'id': 34567,
|
||||||
|
'orderId': 123456,
|
||||||
|
'price': '0.24544100',
|
||||||
|
'qty': '8.00000000',
|
||||||
|
'commission': '0.00800000',
|
||||||
|
'commissionAsset': 'LTC',
|
||||||
|
'time': 1521663363189,
|
||||||
|
'isBuyer': True,
|
||||||
|
'isMaker': False,
|
||||||
|
'isBestMatch': True},
|
||||||
|
'timestamp': 1521663363189,
|
||||||
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
|
'symbol': 'LTC/ETH',
|
||||||
|
'id': '34567',
|
||||||
|
'order': '123456',
|
||||||
|
'type': None,
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 0.245441,
|
||||||
|
'cost': 1.963528,
|
||||||
|
'amount': 4.0,
|
||||||
|
'fee': {'cost': 0.004, 'currency': 'LTC'}},
|
||||||
|
{'info': {'id': 34567,
|
||||||
|
'orderId': 123456,
|
||||||
|
'price': '0.24544100',
|
||||||
|
'qty': '8.00000000',
|
||||||
|
'commission': '0.00800000',
|
||||||
|
'commissionAsset': 'LTC',
|
||||||
|
'time': 1521663363189,
|
||||||
|
'isBuyer': True,
|
||||||
|
'isMaker': False,
|
||||||
|
'isBestMatch': True},
|
||||||
|
'timestamp': 1521663363189,
|
||||||
|
'datetime': '2018-03-21T20:16:03.189Z',
|
||||||
|
'symbol': 'LTC/ETH',
|
||||||
|
'id': '34567',
|
||||||
|
'order': '123456',
|
||||||
|
'type': None,
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 0.245441,
|
||||||
|
'cost': 1.963528,
|
||||||
|
'amount': 4.0,
|
||||||
|
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def get_market_summaries_data():
|
def buy_order_fee():
|
||||||
"""
|
return {
|
||||||
This fixture is a real result from exchange.get_market_summaries() but reduced to only
|
'id': 'mocked_limit_buy_old',
|
||||||
8 entries. 4 BTC, 4 USTD
|
'type': 'limit',
|
||||||
:return: JSON market summaries
|
'side': 'buy',
|
||||||
"""
|
'pair': 'mocked',
|
||||||
return [
|
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
|
||||||
{
|
'price': 0.245441,
|
||||||
'Ask': 1.316e-05,
|
'amount': 8.0,
|
||||||
'BaseVolume': 5.72599471,
|
'remaining': 90.99181073,
|
||||||
'Bid': 1.3e-05,
|
'status': 'closed',
|
||||||
'Created': '2014-04-14T00:00:00',
|
'fee': None
|
||||||
'High': 1.414e-05,
|
}
|
||||||
'Last': 1.298e-05,
|
|
||||||
'Low': 1.282e-05,
|
|
||||||
'MarketName': 'BTC-XWC',
|
|
||||||
'OpenBuyOrders': 2000,
|
|
||||||
'OpenSellOrders': 1484,
|
|
||||||
'PrevDay': 1.376e-05,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:40.493',
|
|
||||||
'Volume': 424041.21418375
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 0.00627051,
|
|
||||||
'BaseVolume': 93.23302388,
|
|
||||||
'Bid': 0.00618192,
|
|
||||||
'Created': '2016-10-20T04:48:30.387',
|
|
||||||
'High': 0.00669897,
|
|
||||||
'Last': 0.00618192,
|
|
||||||
'Low': 0.006,
|
|
||||||
'MarketName': 'BTC-XZC',
|
|
||||||
'OpenBuyOrders': 343,
|
|
||||||
'OpenSellOrders': 2037,
|
|
||||||
'PrevDay': 0.00668229,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:43.383',
|
|
||||||
'Volume': 14863.60730702
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 0.01137247,
|
|
||||||
'BaseVolume': 383.55922657,
|
|
||||||
'Bid': 0.01136006,
|
|
||||||
'Created': '2016-11-15T20:29:59.73',
|
|
||||||
'High': 0.012,
|
|
||||||
'Last': 0.01137247,
|
|
||||||
'Low': 0.01119883,
|
|
||||||
'MarketName': 'BTC-ZCL',
|
|
||||||
'OpenBuyOrders': 1332,
|
|
||||||
'OpenSellOrders': 5317,
|
|
||||||
'PrevDay': 0.01179603,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:42.773',
|
|
||||||
'Volume': 33308.07358285
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 0.04155821,
|
|
||||||
'BaseVolume': 274.75369074,
|
|
||||||
'Bid': 0.04130002,
|
|
||||||
'Created': '2016-10-28T17:13:10.833',
|
|
||||||
'High': 0.04354429,
|
|
||||||
'Last': 0.041585,
|
|
||||||
'Low': 0.0413,
|
|
||||||
'MarketName': 'BTC-ZEC',
|
|
||||||
'OpenBuyOrders': 863,
|
|
||||||
'OpenSellOrders': 5579,
|
|
||||||
'PrevDay': 0.0429,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:43.21',
|
|
||||||
'Volume': 6479.84033259
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 210.99999999,
|
|
||||||
'BaseVolume': 615132.70989532,
|
|
||||||
'Bid': 210.05503736,
|
|
||||||
'Created': '2017-07-21T01:08:49.397',
|
|
||||||
'High': 257.396,
|
|
||||||
'Last': 211.0,
|
|
||||||
'Low': 209.05333589,
|
|
||||||
'MarketName': 'USDT-XMR',
|
|
||||||
'OpenBuyOrders': 180,
|
|
||||||
'OpenSellOrders': 1203,
|
|
||||||
'PrevDay': 247.93528899,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:43.117',
|
|
||||||
'Volume': 2688.17410793
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 0.79589979,
|
|
||||||
'BaseVolume': 9349557.01853031,
|
|
||||||
'Bid': 0.789226,
|
|
||||||
'Created': '2017-07-14T17:10:10.737',
|
|
||||||
'High': 0.977,
|
|
||||||
'Last': 0.79589979,
|
|
||||||
'Low': 0.781,
|
|
||||||
'MarketName': 'USDT-XRP',
|
|
||||||
'OpenBuyOrders': 1075,
|
|
||||||
'OpenSellOrders': 6508,
|
|
||||||
'PrevDay': 0.93300218,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:42.383',
|
|
||||||
'Volume': 10801663.00788851
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 0.05154982,
|
|
||||||
'BaseVolume': 2311087.71232136,
|
|
||||||
'Bid': 0.05040107,
|
|
||||||
'Created': '2017-12-29T19:29:18.357',
|
|
||||||
'High': 0.06668561,
|
|
||||||
'Last': 0.0508,
|
|
||||||
'Low': 0.05006731,
|
|
||||||
'MarketName': 'USDT-XVG',
|
|
||||||
'OpenBuyOrders': 655,
|
|
||||||
'OpenSellOrders': 5544,
|
|
||||||
'PrevDay': 0.0627,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:41.507',
|
|
||||||
'Volume': 40031424.2152716
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'Ask': 332.65500022,
|
|
||||||
'BaseVolume': 562911.87455665,
|
|
||||||
'Bid': 330.00000001,
|
|
||||||
'Created': '2017-07-14T17:10:10.673',
|
|
||||||
'High': 401.59999999,
|
|
||||||
'Last': 332.65500019,
|
|
||||||
'Low': 330.0,
|
|
||||||
'MarketName': 'USDT-ZEC',
|
|
||||||
'OpenBuyOrders': 161,
|
|
||||||
'OpenSellOrders': 1731,
|
|
||||||
'PrevDay': 391.42,
|
|
||||||
'TimeStamp': '2018-02-05T01:32:42.947',
|
|
||||||
'Volume': 1571.09647946
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
@ -1,33 +1,22 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
# pragma pylint: disable=protected-access
|
# pragma pylint: disable=protected-access
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from datetime import datetime
|
||||||
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
from requests.exceptions import RequestException
|
|
||||||
|
|
||||||
import freqtrade.exchange as exchange
|
from freqtrade import OperationalException, DependencyException, TemporaryError
|
||||||
from freqtrade import OperationalException
|
from freqtrade.exchange import Exchange, API_RETRY_COUNT
|
||||||
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||||
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
|
|
||||||
from freqtrade.tests.conftest import log_has
|
|
||||||
|
|
||||||
API_INIT = False
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_init_api(conf, mocker, force=False):
|
|
||||||
global API_INIT
|
|
||||||
if force or not API_INIT:
|
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
|
||||||
side_effect=lambda s: True)
|
|
||||||
init(config=conf)
|
|
||||||
API_INIT = True
|
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
maybe_init_api(default_conf, mocker, True)
|
get_patched_exchange(mocker, default_conf)
|
||||||
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
@ -37,250 +26,706 @@ def test_init_exception(default_conf):
|
|||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||||
init(config=default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker):
|
def test_validate_pairs(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(return_value=[
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
|
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||||
])
|
})
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
type(api_mock).id = id_mock
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_not_available(default_conf, mocker):
|
def test_validate_pairs_not_available(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(return_value=[])
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
with pytest.raises(OperationalException, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_not_compatible(default_conf, mocker):
|
def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
|
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
|
||||||
default_conf['stake_currency'] = 'ETH'
|
})
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
conf['stake_currency'] = 'ETH'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
Exchange(conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(side_effect=RequestException())
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
|
||||||
|
|
||||||
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
Exchange(default_conf)
|
||||||
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
|
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['stake_currency'] = 'ETH'
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.name = MagicMock(return_value='binance')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
|
||||||
|
):
|
||||||
|
Exchange(conf)
|
||||||
|
|
||||||
|
|
||||||
def test_buy_dry_run(default_conf, mocker):
|
def test_buy_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
|
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'dry_run_buy_' in order['id']
|
||||||
|
|
||||||
|
|
||||||
def test_buy_prod(default_conf, mocker):
|
def test_buy_prod(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.buy = MagicMock(
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
|
api_mock.create_limit_buy_order = MagicMock(return_value={
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
|
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
|
||||||
def test_sell_dry_run(default_conf, mocker):
|
def test_sell_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
|
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'dry_run_sell_' in order['id']
|
||||||
|
|
||||||
|
|
||||||
def test_sell_prod(default_conf, mocker):
|
def test_sell_prod(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.sell = MagicMock(
|
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||||
return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
|
api_mock.create_limit_sell_order = MagicMock(return_value={
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
|
|
||||||
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
||||||
|
|
||||||
|
|
||||||
def test_get_balance_dry_run(default_conf, mocker):
|
def test_get_balance_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
|
|
||||||
assert get_balance(currency='BTC') == 999.9
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert exchange.get_balance(currency='BTC') == 999.9
|
||||||
|
|
||||||
|
|
||||||
def test_get_balance_prod(default_conf, mocker):
|
def test_get_balance_prod(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_balance = MagicMock(return_value=123.4)
|
api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}})
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
|
|
||||||
assert get_balance(currency='BTC') == 123.4
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
assert exchange.get_balance(currency='BTC') == 123.4
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
exchange.get_balance(currency='BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_balances_dry_run(default_conf, mocker):
|
def test_get_balances_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert exchange.get_balances() == {}
|
||||||
assert get_balances() == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_balances_prod(default_conf, mocker):
|
def test_get_balances_prod(default_conf, mocker):
|
||||||
balance_item = {
|
balance_item = {
|
||||||
'Currency': '1ST',
|
'free': 10.0,
|
||||||
'Balance': 10.0,
|
'total': 10.0,
|
||||||
'Available': 10.0,
|
'used': 0.0
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_balances = MagicMock(
|
api_mock.fetch_balance = MagicMock(return_value={
|
||||||
return_value=[balance_item, balance_item, balance_item])
|
'1ST': balance_item,
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
'2ST': balance_item,
|
||||||
|
'3ST': balance_item
|
||||||
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
assert len(exchange.get_balances()) == 3
|
||||||
|
assert exchange.get_balances()['1ST']['free'] == 10.0
|
||||||
|
assert exchange.get_balances()['1ST']['total'] == 10.0
|
||||||
|
assert exchange.get_balances()['1ST']['used'] == 0.0
|
||||||
|
|
||||||
assert len(get_balances()) == 3
|
with pytest.raises(TemporaryError):
|
||||||
assert get_balances()[0]['Currency'] == '1ST'
|
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
assert get_balances()[0]['Balance'] == 10.0
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
assert get_balances()[0]['Available'] == 10.0
|
exchange.get_balances()
|
||||||
assert get_balances()[0]['Pending'] == 0.0
|
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_balances()
|
||||||
|
assert api_mock.fetch_balance.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
# This test is somewhat redundant with
|
def test_get_tickers(default_conf, mocker):
|
||||||
# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker
|
|
||||||
def test_get_ticker(default_conf, mocker):
|
|
||||||
maybe_init_api(default_conf, mocker)
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
|
tick = {'ETH/BTC': {
|
||||||
api_mock.get_ticker = MagicMock(return_value=tick)
|
'symbol': 'ETH/BTC',
|
||||||
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
|
'bid': 0.5,
|
||||||
|
'ask': 1,
|
||||||
|
'last': 42,
|
||||||
|
}, 'BCH/BTC': {
|
||||||
|
'symbol': 'BCH/BTC',
|
||||||
|
'bid': 0.6,
|
||||||
|
'ask': 0.5,
|
||||||
|
'last': 41,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api_mock.fetch_tickers = MagicMock(return_value=tick)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
# retrieve original ticker
|
# retrieve original ticker
|
||||||
ticker = get_ticker(pair='BTC_ETH')
|
tickers = exchange.get_tickers()
|
||||||
|
|
||||||
|
assert 'ETH/BTC' in tickers
|
||||||
|
assert 'BCH/BTC' in tickers
|
||||||
|
assert tickers['ETH/BTC']['bid'] == 0.5
|
||||||
|
assert tickers['ETH/BTC']['ask'] == 1
|
||||||
|
assert tickers['BCH/BTC']['bid'] == 0.6
|
||||||
|
assert tickers['BCH/BTC']['ask'] == 0.5
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError): # test retrier
|
||||||
|
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_tickers()
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_tickers()
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_tickers()
|
||||||
|
|
||||||
|
api_mock.fetch_tickers = MagicMock(return_value={})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_tickers()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ticker(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
tick = {
|
||||||
|
'symbol': 'ETH/BTC',
|
||||||
|
'bid': 0.00001098,
|
||||||
|
'ask': 0.00001099,
|
||||||
|
'last': 0.0001,
|
||||||
|
}
|
||||||
|
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
# retrieve original ticker
|
||||||
|
ticker = exchange.get_ticker(pair='ETH/BTC')
|
||||||
|
|
||||||
assert ticker['bid'] == 0.00001098
|
assert ticker['bid'] == 0.00001098
|
||||||
assert ticker['ask'] == 0.00001099
|
assert ticker['ask'] == 0.00001099
|
||||||
|
|
||||||
# change the ticker
|
# change the ticker
|
||||||
tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}}
|
tick = {
|
||||||
api_mock.get_ticker = MagicMock(return_value=tick)
|
'symbol': 'ETH/BTC',
|
||||||
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
|
'bid': 0.5,
|
||||||
|
'ask': 1,
|
||||||
|
'last': 42,
|
||||||
|
}
|
||||||
|
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
# if not caching the result we should get the same ticker
|
# if not caching the result we should get the same ticker
|
||||||
# if not fetching a new result we should get the cached ticker
|
# if not fetching a new result we should get the cached ticker
|
||||||
ticker = get_ticker(pair='BTC_ETH', refresh=False)
|
ticker = exchange.get_ticker(pair='ETH/BTC')
|
||||||
assert ticker['bid'] == 0.00001098
|
|
||||||
assert ticker['ask'] == 0.00001099
|
|
||||||
|
|
||||||
# force ticker refresh
|
assert api_mock.fetch_ticker.call_count == 1
|
||||||
ticker = get_ticker(pair='BTC_ETH', refresh=True)
|
|
||||||
assert ticker['bid'] == 0.5
|
assert ticker['bid'] == 0.5
|
||||||
assert ticker['ask'] == 1
|
assert ticker['ask'] == 1
|
||||||
|
|
||||||
|
assert 'ETH/BTC' in exchange._cached_ticker
|
||||||
|
assert exchange._cached_ticker['ETH/BTC']['bid'] == 0.5
|
||||||
|
assert exchange._cached_ticker['ETH/BTC']['ask'] == 1
|
||||||
|
|
||||||
|
# Test caching
|
||||||
|
api_mock.fetch_ticker = MagicMock()
|
||||||
|
exchange.get_ticker(pair='ETH/BTC', refresh=False)
|
||||||
|
assert api_mock.fetch_ticker.call_count == 0
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError): # test retrier
|
||||||
|
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
|
||||||
|
def make_fetch_ohlcv_mock(data):
|
||||||
|
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||||
|
if since:
|
||||||
|
assert since > data[-1][0]
|
||||||
|
return []
|
||||||
|
return data
|
||||||
|
return fetch_ohlcv_mock
|
||||||
|
|
||||||
|
|
||||||
def test_get_ticker_history(default_conf, mocker):
|
def test_get_ticker_history(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
tick = 123
|
tick = [
|
||||||
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
[
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
1511686200000, # unix timestamp ms
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
# retrieve original ticker
|
# retrieve original ticker
|
||||||
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert ticks == 123
|
assert ticks[0][0] == 1511686200000
|
||||||
|
assert ticks[0][1] == 1
|
||||||
|
assert ticks[0][2] == 2
|
||||||
|
assert ticks[0][3] == 3
|
||||||
|
assert ticks[0][4] == 4
|
||||||
|
assert ticks[0][5] == 5
|
||||||
|
|
||||||
# change the ticker
|
# change ticker and ensure tick changes
|
||||||
tick = 999
|
new_tick = [
|
||||||
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
[
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
1511686210000, # unix timestamp ms
|
||||||
|
6, # open
|
||||||
|
7, # high
|
||||||
|
8, # low
|
||||||
|
9, # close
|
||||||
|
10, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
# ensure caching will still return the original ticker
|
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
assert ticks[0][0] == 1511686210000
|
||||||
assert ticks == 123
|
assert ticks[0][1] == 6
|
||||||
|
assert ticks[0][2] == 7
|
||||||
|
assert ticks[0][3] == 8
|
||||||
|
assert ticks[0][4] == 9
|
||||||
|
assert ticks[0][5] == 10
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError): # test retrier
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
# new symbol to get around cache
|
||||||
|
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
# new symbol to get around cache
|
||||||
|
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ticker_history_sort(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
# GDAX use-case (real data from GDAX)
|
||||||
|
# This ticker history is ordered DESC (newest first, oldest last)
|
||||||
|
tick = [
|
||||||
|
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
|
||||||
|
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
|
||||||
|
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
|
||||||
|
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
|
||||||
|
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
|
||||||
|
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
|
||||||
|
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
|
||||||
|
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
|
||||||
|
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
||||||
|
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
||||||
|
]
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
# Test the ticker history sort
|
||||||
|
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert ticks[0][0] == 1527830400000
|
||||||
|
assert ticks[0][1] == 0.07649
|
||||||
|
assert ticks[0][2] == 0.07651
|
||||||
|
assert ticks[0][3] == 0.07649
|
||||||
|
assert ticks[0][4] == 0.07651
|
||||||
|
assert ticks[0][5] == 2.5734867
|
||||||
|
|
||||||
|
assert ticks[9][0] == 1527833100000
|
||||||
|
assert ticks[9][1] == 0.07666
|
||||||
|
assert ticks[9][2] == 0.07671
|
||||||
|
assert ticks[9][3] == 0.07666
|
||||||
|
assert ticks[9][4] == 0.07668
|
||||||
|
assert ticks[9][5] == 16.65244264
|
||||||
|
|
||||||
|
# Bittrex use-case (real data from Bittrex)
|
||||||
|
# This ticker history is ordered ASC (oldest first, newest last)
|
||||||
|
tick = [
|
||||||
|
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
|
||||||
|
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
|
||||||
|
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
|
||||||
|
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
|
||||||
|
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
|
||||||
|
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
|
||||||
|
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
|
||||||
|
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
|
||||||
|
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
|
||||||
|
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
|
||||||
|
]
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
# Test the ticker history sort
|
||||||
|
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert ticks[0][0] == 1527827700000
|
||||||
|
assert ticks[0][1] == 0.07659999
|
||||||
|
assert ticks[0][2] == 0.0766
|
||||||
|
assert ticks[0][3] == 0.07627
|
||||||
|
assert ticks[0][4] == 0.07657998
|
||||||
|
assert ticks[0][5] == 1.85216924
|
||||||
|
|
||||||
|
assert ticks[9][0] == 1527830400000
|
||||||
|
assert ticks[9][1] == 0.07671
|
||||||
|
assert ticks[9][2] == 0.07674399
|
||||||
|
assert ticks[9][3] == 0.07629216
|
||||||
|
assert ticks[9][4] == 0.07655213
|
||||||
|
assert ticks[9][5] == 2.31452783
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_order_dry_run(default_conf, mocker):
|
def test_cancel_order_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
|
||||||
assert cancel_order(order_id='123') is None
|
|
||||||
|
|
||||||
|
|
||||||
# Ensure that if not dry_run, we should call API
|
# Ensure that if not dry_run, we should call API
|
||||||
def test_cancel_order(default_conf, mocker):
|
def test_cancel_order(default_conf, mocker):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.cancel_order = MagicMock(return_value=123)
|
api_mock.cancel_order = MagicMock(return_value=123)
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
assert cancel_order(order_id='_') == 123
|
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.cancel_order.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_order(default_conf, mocker):
|
def test_get_order(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
print(exchange.get_order('X'))
|
exchange._dry_run_open_orders['X'] = order
|
||||||
assert exchange.get_order('X').myid == 123
|
print(exchange.get_order('X', 'TKN/BTC'))
|
||||||
|
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_order = MagicMock(return_value=456)
|
api_mock.fetch_order = MagicMock(return_value=456)
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
assert exchange.get_order('X') == 456
|
assert exchange.get_order('X', 'TKN/BTC') == 456
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||||
|
assert api_mock.fetch_order.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_name(default_conf, mocker):
|
def test_name(default_conf, mocker):
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||||
side_effect=lambda s: True)
|
side_effect=lambda s: True)
|
||||||
default_conf['exchange']['name'] = 'bittrex'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
init(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
assert get_name() == 'Bittrex'
|
assert exchange.name == 'Binance'
|
||||||
|
|
||||||
|
|
||||||
|
def test_id(default_conf, mocker):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||||
|
side_effect=lambda s: True)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
assert exchange.id == 'binance'
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pair_detail_url(default_conf, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
||||||
|
side_effect=lambda s: True)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
url = exchange.get_pair_detail_url('TKN/ETH')
|
||||||
|
assert 'TKN' in url
|
||||||
|
assert 'ETH' in url
|
||||||
|
|
||||||
|
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||||
|
assert 'LOOONG' in url
|
||||||
|
assert 'BTC' in url
|
||||||
|
|
||||||
|
default_conf['exchange']['name'] = 'bittrex'
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
url = exchange.get_pair_detail_url('TKN/ETH')
|
||||||
|
assert 'TKN' in url
|
||||||
|
assert 'ETH' in url
|
||||||
|
|
||||||
|
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||||
|
assert 'LOOONG' in url
|
||||||
|
assert 'BTC' in url
|
||||||
|
|
||||||
|
default_conf['exchange']['name'] = 'poloniex'
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||||
|
assert '' == url
|
||||||
|
assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_trades_for_order(default_conf, mocker):
|
||||||
|
order_id = 'ABCD-ABCD'
|
||||||
|
since = datetime(2018, 5, 5)
|
||||||
|
default_conf["dry_run"] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
|
||||||
|
'order': 'ABCD-ABCD',
|
||||||
|
'info': {'pair': 'XLTCZBTC',
|
||||||
|
'time': 1519860024.4388,
|
||||||
|
'type': 'buy',
|
||||||
|
'ordertype': 'limit',
|
||||||
|
'price': '20.00000',
|
||||||
|
'cost': '38.62000',
|
||||||
|
'fee': '0.06179',
|
||||||
|
'vol': '5',
|
||||||
|
'id': 'ABCD-ABCD'},
|
||||||
|
'timestamp': 1519860024438,
|
||||||
|
'datetime': '2018-02-28T23:20:24.438Z',
|
||||||
|
'symbol': 'LTC/BTC',
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'price': 165.0,
|
||||||
|
'amount': 0.2340606,
|
||||||
|
'fee': {'cost': 0.06179, 'currency': 'BTC'}
|
||||||
|
}])
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||||
|
assert len(orders) == 1
|
||||||
|
assert orders[0]['price'] == 165
|
||||||
|
|
||||||
|
# test Exceptions
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||||
|
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_markets(default_conf, mocker, markets):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_markets = markets
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
ret = exchange.get_markets()
|
||||||
|
assert isinstance(ret, list)
|
||||||
|
assert len(ret) == 6
|
||||||
|
|
||||||
|
assert ret[0]["id"] == "ethbtc"
|
||||||
|
assert ret[0]["symbol"] == "ETH/BTC"
|
||||||
|
|
||||||
|
# test Exceptions
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_markets()
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_markets()
|
||||||
|
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
|
||||||
def test_get_fee(default_conf, mocker):
|
def test_get_fee(default_conf, mocker):
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
|
||||||
side_effect=lambda s: True)
|
|
||||||
init(default_conf)
|
|
||||||
|
|
||||||
assert get_fee() == 0.0025
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_misc(mocker):
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
api_mock.calculate_fee = MagicMock(return_value={
|
||||||
exchange.get_markets()
|
'type': 'taker',
|
||||||
assert api_mock.get_markets.call_count == 1
|
'currency': 'BTC',
|
||||||
exchange.get_market_summaries()
|
'rate': 0.025,
|
||||||
assert api_mock.get_market_summaries.call_count == 1
|
'cost': 0.05
|
||||||
api_mock.name = 123
|
})
|
||||||
assert exchange.get_name() == 123
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
api_mock.fee = 456
|
|
||||||
assert exchange.get_fee() == 456
|
assert exchange.get_fee() == 0.025
|
||||||
exchange.get_wallet_health()
|
|
||||||
assert api_mock.get_wallet_health.call_count == 1
|
# test Exceptions
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_fee()
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_fee()
|
||||||
|
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_amount_lots(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.amount_to_lots = MagicMock(return_value=1.0)
|
||||||
|
api_mock.markets = None
|
||||||
|
marketmock = MagicMock()
|
||||||
|
api_mock.load_markets = marketmock
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
|
||||||
|
assert marketmock.call_count == 1
|
||||||
|
@ -1,349 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
|
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from requests.exceptions import ContentDecodingError
|
|
||||||
|
|
||||||
import freqtrade.exchange.bittrex as btx
|
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
|
|
||||||
|
|
||||||
# Eat this flake8
|
|
||||||
# +------------------+
|
|
||||||
# | bittrex.Bittrex |
|
|
||||||
# +------------------+
|
|
||||||
# |
|
|
||||||
# (mock Fake_bittrex)
|
|
||||||
# |
|
|
||||||
# +-----------------------------+
|
|
||||||
# | freqtrade.exchange.Bittrex |
|
|
||||||
# +-----------------------------+
|
|
||||||
# Call into Bittrex will flow up to the
|
|
||||||
# external package bittrex.Bittrex.
|
|
||||||
# By inserting a mock, we redirect those
|
|
||||||
# calls.
|
|
||||||
# The faked bittrex API is called just 'fb'
|
|
||||||
# The freqtrade.exchange.Bittrex is a
|
|
||||||
# wrapper, and is called 'wb'
|
|
||||||
|
|
||||||
|
|
||||||
def _stub_config():
|
|
||||||
return {'key': '',
|
|
||||||
'secret': ''}
|
|
||||||
|
|
||||||
|
|
||||||
class FakeBittrex():
|
|
||||||
def __init__(self, success=True):
|
|
||||||
self.success = True # Believe in yourself
|
|
||||||
self.result = None
|
|
||||||
self.get_ticker_call_count = 0
|
|
||||||
# This is really ugly, doing side-effect during instance creation
|
|
||||||
# But we're allowed to in testing-code
|
|
||||||
btx._API = MagicMock()
|
|
||||||
btx._API.buy_limit = self.fake_buysell_limit
|
|
||||||
btx._API.sell_limit = self.fake_buysell_limit
|
|
||||||
btx._API.get_balance = self.fake_get_balance
|
|
||||||
btx._API.get_balances = self.fake_get_balances
|
|
||||||
btx._API.get_ticker = self.fake_get_ticker
|
|
||||||
btx._API.get_order = self.fake_get_order
|
|
||||||
btx._API.cancel = self.fake_cancel_order
|
|
||||||
btx._API.get_markets = self.fake_get_markets
|
|
||||||
btx._API.get_market_summaries = self.fake_get_market_summaries
|
|
||||||
btx._API_V2 = MagicMock()
|
|
||||||
btx._API_V2.get_candles = self.fake_get_candles
|
|
||||||
btx._API_V2.get_wallet_health = self.fake_get_wallet_health
|
|
||||||
|
|
||||||
def fake_buysell_limit(self, pair, amount, limit):
|
|
||||||
return {'success': self.success,
|
|
||||||
'result': {'uuid': '1234'},
|
|
||||||
'message': 'barter'}
|
|
||||||
|
|
||||||
def fake_get_balance(self, cur):
|
|
||||||
return {'success': self.success,
|
|
||||||
'result': {'Balance': 1234},
|
|
||||||
'message': 'unbalanced'}
|
|
||||||
|
|
||||||
def fake_get_balances(self):
|
|
||||||
return {'success': self.success,
|
|
||||||
'result': [{'BTC_ETH': 1234}],
|
|
||||||
'message': 'no balances'}
|
|
||||||
|
|
||||||
def fake_get_ticker(self, pair):
|
|
||||||
self.get_ticker_call_count += 1
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'result': {'Bid': 1, 'Ask': 1, 'Last': 1},
|
|
||||||
'message': 'NO_API_RESPONSE'}
|
|
||||||
|
|
||||||
def fake_get_candles(self, pair, interval):
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],
|
|
||||||
'message': 'candles lit'}
|
|
||||||
|
|
||||||
def fake_get_order(self, uuid):
|
|
||||||
return {'success': self.success,
|
|
||||||
'result': {'OrderUuid': 'ABC123',
|
|
||||||
'Type': 'Type',
|
|
||||||
'Exchange': 'BTC_ETH',
|
|
||||||
'Opened': True,
|
|
||||||
'PricePerUnit': 1,
|
|
||||||
'Quantity': 1,
|
|
||||||
'QuantityRemaining': 1,
|
|
||||||
'Closed': True},
|
|
||||||
'message': 'lost'}
|
|
||||||
|
|
||||||
def fake_cancel_order(self, uuid):
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'message': 'no such order'}
|
|
||||||
|
|
||||||
def fake_get_markets(self):
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'message': 'market gone',
|
|
||||||
'result': [{'MarketName': '-_'}]}
|
|
||||||
|
|
||||||
def fake_get_market_summaries(self):
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'message': 'no summary',
|
|
||||||
'result': ['sum']}
|
|
||||||
|
|
||||||
def fake_get_wallet_health(self):
|
|
||||||
return self.result or {'success': self.success,
|
|
||||||
'message': 'bad health',
|
|
||||||
'result': [{'Health': {'Currency': 'BTC_ETH',
|
|
||||||
'IsActive': True,
|
|
||||||
'LastChecked': 0},
|
|
||||||
'Currency': {'Notice': True}}]}
|
|
||||||
|
|
||||||
|
|
||||||
# The freqtrade.exchange.bittrex is called wrap_bittrex
|
|
||||||
# to not confuse naming with bittrex.bittrex
|
|
||||||
def make_wrap_bittrex():
|
|
||||||
conf = _stub_config()
|
|
||||||
wb = btx.Bittrex(conf)
|
|
||||||
return wb
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_class():
|
|
||||||
conf = _stub_config()
|
|
||||||
b = Bittrex(conf)
|
|
||||||
assert isinstance(b, Bittrex)
|
|
||||||
slots = dir(b)
|
|
||||||
for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
|
|
||||||
'get_ticker', 'get_ticker_history', 'get_order',
|
|
||||||
'cancel_order', 'get_pair_detail_url', 'get_markets',
|
|
||||||
'get_market_summaries', 'get_wallet_health']:
|
|
||||||
assert name in slots
|
|
||||||
# FIX: ensure that the slot is also a method in the class
|
|
||||||
# getattr(b, name) => bound method Bittrex.buy
|
|
||||||
# type(getattr(b, name)) => class 'method'
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_fee():
|
|
||||||
fee = Bittrex.fee.__get__(Bittrex)
|
|
||||||
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_buy_good():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
uuid = wb.buy('BTC_ETH', 1, 1)
|
|
||||||
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
|
|
||||||
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'barter.*'):
|
|
||||||
wb.buy('BAD', 1, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_sell_good():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
uuid = wb.sell('BTC_ETH', 1, 1)
|
|
||||||
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
|
|
||||||
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'barter.*'):
|
|
||||||
uuid = wb.sell('BAD', 1, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_balance():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
bal = wb.get_balance('BTC_ETH')
|
|
||||||
assert bal == fb.fake_get_balance(1)['result']['Balance']
|
|
||||||
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'unbalanced'):
|
|
||||||
wb.get_balance('BTC_ETH')
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_balances():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
bals = wb.get_balances()
|
|
||||||
assert bals == fb.fake_get_balances()['result']
|
|
||||||
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'no balances'):
|
|
||||||
wb.get_balances()
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
|
|
||||||
# Poll ticker, which updates the cache
|
|
||||||
tick = wb.get_ticker('BTC_ETH')
|
|
||||||
for x in ['bid', 'ask', 'last']:
|
|
||||||
assert x in tick
|
|
||||||
# Ensure the side-effect was made (update the ticker cache)
|
|
||||||
assert 'BTC_ETH' in wb.cached_ticker.keys()
|
|
||||||
|
|
||||||
# taint the cache, so we can recognize the cache wall utilized
|
|
||||||
wb.cached_ticker['BTC_ETH']['bid'] = 1234
|
|
||||||
# Poll again, getting the cached result
|
|
||||||
fb.get_ticker_call_count = 0
|
|
||||||
tick = wb.get_ticker('BTC_ETH', False)
|
|
||||||
# Ensure the result was from the cache, and that we didn't call exchange
|
|
||||||
assert wb.cached_ticker['BTC_ETH']['bid'] == 1234
|
|
||||||
assert fb.get_ticker_call_count == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker_bad():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result
|
|
||||||
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
|
||||||
wb.get_ticker('BTC_ETH')
|
|
||||||
fb.result = {'success': False, 'message': 'gone bad'}
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
|
||||||
wb.get_ticker('BTC_ETH')
|
|
||||||
|
|
||||||
fb.result = {'success': True, 'result': {}} # incomplete result
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
|
||||||
wb.get_ticker('BTC_ETH')
|
|
||||||
fb.result = {'success': False, 'message': 'gone bad'}
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
|
||||||
wb.get_ticker('BTC_ETH')
|
|
||||||
|
|
||||||
fb.result = {'success': True,
|
|
||||||
'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
|
||||||
wb.get_ticker('BTC_ETH')
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker_history_intervals():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
FakeBittrex()
|
|
||||||
for tick_interval in [1, 5, 30, 60, 1440]:
|
|
||||||
assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
|
|
||||||
wb.get_ticker_history('BTC_ETH', tick_interval))
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker_history():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
assert wb.get_ticker_history('BTC_ETH', 5)
|
|
||||||
with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'):
|
|
||||||
wb.get_ticker_history('BTC_ETH', 2)
|
|
||||||
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'candles lit.*'):
|
|
||||||
wb.get_ticker_history('BTC_ETH', 5)
|
|
||||||
|
|
||||||
fb.success = True
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'):
|
|
||||||
fb.result = {'bad': 0}
|
|
||||||
wb.get_ticker_history('BTC_ETH', 5)
|
|
||||||
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'):
|
|
||||||
fb.result = {'success': True,
|
|
||||||
'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing
|
|
||||||
'message': 'candles lit'}
|
|
||||||
wb.get_ticker_history('BTC_ETH', 5)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_order():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
order = wb.get_order('someUUID')
|
|
||||||
assert order['id'] == 'ABC123'
|
|
||||||
fb.success = False
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'lost'):
|
|
||||||
wb.get_order('someUUID')
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_cancel_order():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
wb.cancel_order('someUUID')
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'no such order'):
|
|
||||||
fb.success = False
|
|
||||||
wb.cancel_order('someUUID')
|
|
||||||
# Note: this can be a bug in exchange.bittrex._validate_response
|
|
||||||
with pytest.raises(KeyError):
|
|
||||||
fb.result = {'success': False} # message is missing!
|
|
||||||
wb.cancel_order('someUUID')
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'foo'):
|
|
||||||
fb.result = {'success': False, 'message': 'foo'}
|
|
||||||
wb.cancel_order('someUUID')
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_get_pair_detail_url():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
assert wb.get_pair_detail_url('BTC_ETH')
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_get_markets():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
x = wb.get_markets()
|
|
||||||
assert x == ['__']
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'market gone'):
|
|
||||||
fb.success = False
|
|
||||||
wb.get_markets()
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_get_market_summaries():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
assert ['sum'] == wb.get_market_summaries()
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'no summary'):
|
|
||||||
fb.success = False
|
|
||||||
wb.get_market_summaries()
|
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_get_wallet_health():
|
|
||||||
wb = make_wrap_bittrex()
|
|
||||||
fb = FakeBittrex()
|
|
||||||
x = wb.get_wallet_health()
|
|
||||||
assert x[0]['Currency'] == 'BTC_ETH'
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'bad health'):
|
|
||||||
fb.success = False
|
|
||||||
wb.get_wallet_health()
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_response_success():
|
|
||||||
response = {
|
|
||||||
'message': '',
|
|
||||||
'result': [],
|
|
||||||
}
|
|
||||||
Bittrex._validate_response(response)
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_response_no_api_response():
|
|
||||||
response = {
|
|
||||||
'message': 'NO_API_RESPONSE',
|
|
||||||
'result': None,
|
|
||||||
}
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
|
|
||||||
Bittrex._validate_response(response)
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_response_min_trade_requirement_not_met():
|
|
||||||
response = {
|
|
||||||
'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
|
|
||||||
'result': None,
|
|
||||||
}
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
|
|
||||||
Bittrex._validate_response(response)
|
|
@ -3,6 +3,7 @@
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
|
import pytest
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import List
|
from typing import List
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@ -11,14 +12,11 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import optimize
|
from freqtrade import optimize, constants, DependencyException
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||||
from freqtrade.tests.conftest import default_conf, log_has
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
|
||||||
_BACKTESTING = Backtesting(default_conf())
|
|
||||||
|
|
||||||
|
|
||||||
def get_args(args) -> List[str]:
|
def get_args(args) -> List[str]:
|
||||||
@ -33,50 +31,61 @@ def trim_dictlist(dict_list, num):
|
|||||||
|
|
||||||
|
|
||||||
def load_data_test(what):
|
def load_data_test(what):
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = TimeRange(None, 'line', 0, -101)
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
|
data = optimize.load_data(None, ticker_interval='1m',
|
||||||
pair = data['BTC_UNITEST']
|
pairs=['UNITTEST/BTC'], timerange=timerange)
|
||||||
|
pair = data['UNITTEST/BTC']
|
||||||
datalen = len(pair)
|
datalen = len(pair)
|
||||||
# Depending on the what parameter we now adjust the
|
# Depending on the what parameter we now adjust the
|
||||||
# loaded data looks:
|
# loaded data looks:
|
||||||
# pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123,
|
# pair :: [[ 1509836520000, unix timestamp in ms
|
||||||
# 'C': 0.123, 'V': 123.123,
|
# 0.00162008, open
|
||||||
# 'T': '2017-11-04T23:02:00', 'BV': 0.123}]
|
# 0.00162008, high
|
||||||
|
# 0.00162008, low
|
||||||
|
# 0.00162008, close
|
||||||
|
# 108.14853839 base volume
|
||||||
|
# ]]
|
||||||
base = 0.001
|
base = 0.001
|
||||||
if what == 'raise':
|
if what == 'raise':
|
||||||
return {'BTC_UNITEST':
|
return {'UNITTEST/BTC': [
|
||||||
[{'T': pair[x]['T'], # Keep old dates
|
[
|
||||||
'V': pair[x]['V'], # Keep old volume
|
pair[x][0], # Keep old dates
|
||||||
'BV': pair[x]['BV'], # keep too
|
x * base, # But replace O,H,L,C
|
||||||
'O': x * base, # But replace O,H,L,C
|
x * base + 0.0001,
|
||||||
'H': x * base + 0.0001,
|
x * base - 0.0001,
|
||||||
'L': x * base - 0.0001,
|
x * base,
|
||||||
'C': x * base} for x in range(0, datalen)]}
|
pair[x][5], # Keep old volume
|
||||||
|
] for x in range(0, datalen)
|
||||||
|
]}
|
||||||
if what == 'lower':
|
if what == 'lower':
|
||||||
return {'BTC_UNITEST':
|
return {'UNITTEST/BTC': [
|
||||||
[{'T': pair[x]['T'], # Keep old dates
|
[
|
||||||
'V': pair[x]['V'], # Keep old volume
|
pair[x][0], # Keep old dates
|
||||||
'BV': pair[x]['BV'], # keep too
|
1 - x * base, # But replace O,H,L,C
|
||||||
'O': 1 - x * base, # But replace O,H,L,C
|
1 - x * base + 0.0001,
|
||||||
'H': 1 - x * base + 0.0001,
|
1 - x * base - 0.0001,
|
||||||
'L': 1 - x * base - 0.0001,
|
1 - x * base,
|
||||||
'C': 1 - x * base} for x in range(0, datalen)]}
|
pair[x][5] # Keep old volume
|
||||||
|
] for x in range(0, datalen)
|
||||||
|
]}
|
||||||
if what == 'sine':
|
if what == 'sine':
|
||||||
hz = 0.1 # frequency
|
hz = 0.1 # frequency
|
||||||
return {'BTC_UNITEST':
|
return {'UNITTEST/BTC': [
|
||||||
[{'T': pair[x]['T'], # Keep old dates
|
[
|
||||||
'V': pair[x]['V'], # Keep old volume
|
pair[x][0], # Keep old dates
|
||||||
'BV': pair[x]['BV'], # keep too
|
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
|
||||||
# But replace O,H,L,C
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
'O': math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
'H': math.sin(x * hz) / 1000 + base + 0.0001,
|
math.sin(x * hz) / 1000 + base,
|
||||||
'L': math.sin(x * hz) / 1000 + base - 0.0001,
|
pair[x][5] # Keep old volume
|
||||||
'C': math.sin(x * hz) / 1000 + base} for x in range(0, datalen)]}
|
] for x in range(0, datalen)
|
||||||
|
]}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
backtesting = _BACKTESTING
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(config)
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
processed = backtesting.tickerdata_to_dataframe(data)
|
processed = backtesting.tickerdata_to_dataframe(data)
|
||||||
@ -93,26 +102,29 @@ def simple_backtest(config, contour, num_results) -> None:
|
|||||||
assert len(results) == num_results
|
assert len(results) == num_results
|
||||||
|
|
||||||
|
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
|
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||||
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
|
timerange=None, exchange=None):
|
||||||
pairdata = {'BTC_UNITEST': tickerdata}
|
tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
|
pairdata = {'UNITTEST/BTC': tickerdata}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
# use for mock freqtrade.exchange.get_ticker_history'
|
# use for mock freqtrade.exchange.get_ticker_history'
|
||||||
def _load_pair_as_ticks(pair, tickfreq):
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
ticks = trim_dictlist(ticks, -200)
|
ticks = trim_dictlist(ticks, -201)
|
||||||
return ticks[pair]
|
return ticks[pair]
|
||||||
|
|
||||||
|
|
||||||
# FIX: fixturize this?
|
# FIX: fixturize this?
|
||||||
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
|
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
||||||
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
|
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -201)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(conf)
|
||||||
return {
|
return {
|
||||||
'stake_amount': conf['stake_amount'],
|
'stake_amount': conf['stake_amount'],
|
||||||
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'realistic': True,
|
'realistic': True,
|
||||||
'record': record
|
'record': record
|
||||||
@ -148,21 +160,6 @@ def _trend_alternate(dataframe=None):
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def _run_backtest_1(fun, backtest_conf):
|
|
||||||
# strategy is a global (hidden as a singleton), so we
|
|
||||||
# emulate strategy being pure, by override/restore here
|
|
||||||
# if we dont do this, the override in strategy will carry over
|
|
||||||
# to other tests
|
|
||||||
old_buy = _BACKTESTING.populate_buy_trend
|
|
||||||
old_sell = _BACKTESTING.populate_sell_trend
|
|
||||||
_BACKTESTING.populate_buy_trend = fun # Override
|
|
||||||
_BACKTESTING.populate_sell_trend = fun # Override
|
|
||||||
results = _BACKTESTING.backtest(backtest_conf)
|
|
||||||
_BACKTESTING.populate_buy_trend = old_buy # restore override
|
|
||||||
_BACKTESTING.populate_sell_trend = old_sell # restore override
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
@ -186,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -218,12 +215,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--realistic-simulation',
|
'--realistic-simulation',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo',
|
||||||
|
'--export-filename', 'foo_bar.json'
|
||||||
]
|
]
|
||||||
|
|
||||||
config = setup_configuration(get_args(args))
|
config = setup_configuration(get_args(args))
|
||||||
@ -234,24 +232,24 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using ticker_interval: 1 ...',
|
'Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
assert 'live' in config
|
assert 'live' in config
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'realistic_simulation'in config
|
assert 'realistic_simulation' in config
|
||||||
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'refresh_pairs'in config
|
assert 'refresh_pairs' in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
assert 'timerange' in config
|
assert 'timerange' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -264,13 +262,42 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'Parameter --export detected: {} ...'.format(config['export']),
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
assert 'exportfilename' in config
|
||||||
|
assert log_has(
|
||||||
|
'Storing backtest results to {} ...'.format(config['exportfilename']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_start(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
|
setup_configuration(get_args(args))
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test start() function
|
Test start() function
|
||||||
"""
|
"""
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
@ -289,114 +316,104 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting__init__(mocker, default_conf) -> None:
|
def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.__init__() method
|
|
||||||
"""
|
|
||||||
init_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
|
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
assert backtesting.config == default_conf
|
|
||||||
assert backtesting.analyze is None
|
|
||||||
assert backtesting.ticker_interval is None
|
|
||||||
assert backtesting.tickerdata_to_dataframe is None
|
|
||||||
assert backtesting.populate_buy_trend is None
|
|
||||||
assert backtesting.populate_sell_trend is None
|
|
||||||
assert init_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_init(default_conf) -> None:
|
|
||||||
"""
|
"""
|
||||||
Test Backtesting._init() method
|
Test Backtesting._init() method
|
||||||
"""
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
assert backtesting.config == default_conf
|
assert backtesting.config == default_conf
|
||||||
assert isinstance(backtesting.analyze, Analyze)
|
assert isinstance(backtesting.analyze, Analyze)
|
||||||
assert backtesting.ticker_interval == 5
|
assert backtesting.ticker_interval == '5m'
|
||||||
assert callable(backtesting.tickerdata_to_dataframe)
|
assert callable(backtesting.tickerdata_to_dataframe)
|
||||||
assert callable(backtesting.populate_buy_trend)
|
assert callable(backtesting.populate_buy_trend)
|
||||||
assert callable(backtesting.populate_sell_trend)
|
assert callable(backtesting.populate_sell_trend)
|
||||||
|
get_fee.assert_called()
|
||||||
|
assert backtesting.fee == 0.5
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.tickerdata_to_dataframe() method
|
Test Backtesting.tickerdata_to_dataframe() method
|
||||||
"""
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
|
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
|
|
||||||
timerange = ((None, 'line'), None, -100)
|
backtesting = Backtesting(default_conf)
|
||||||
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
|
||||||
|
|
||||||
backtesting = _BACKTESTING
|
|
||||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['BTC_UNITEST']) == 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)
|
||||||
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
|
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe() -> None:
|
def test_get_timeframe(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.get_timeframe() method
|
Test Backtesting.get_timeframe() method
|
||||||
"""
|
"""
|
||||||
backtesting = _BACKTESTING
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
data = backtesting.tickerdata_to_dataframe(
|
data = backtesting.tickerdata_to_dataframe(
|
||||||
optimize.load_data(
|
optimize.load_data(
|
||||||
None,
|
None,
|
||||||
ticker_interval=1,
|
ticker_interval='1m',
|
||||||
pairs=['BTC_UNITEST']
|
pairs=['UNITTEST/BTC']
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
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():
|
def test_generate_text_table(default_conf, mocker):
|
||||||
"""
|
"""
|
||||||
Test Backtesting.generate_text_table() method
|
Test Backtesting.generate_text_table() method
|
||||||
"""
|
"""
|
||||||
backtesting = _BACKTESTING
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
'currency': ['BTC_ETH', 'BTC_ETH'],
|
'pair': ['ETH/BTC', 'ETH/BTC'],
|
||||||
'profit_percent': [0.1, 0.2],
|
'profit_percent': [0.1, 0.2],
|
||||||
'profit_BTC': [0.2, 0.4],
|
'profit_abs': [0.2, 0.4],
|
||||||
'duration': [10, 30],
|
'trade_duration': [10, 30],
|
||||||
'profit': [2, 0],
|
'profit': [2, 0],
|
||||||
'loss': [0, 0]
|
'loss': [0, 0]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'pair buy count avg profit % '
|
'| pair | buy count | avg profit % | '
|
||||||
'total profit BTC avg duration profit loss\n'
|
'total profit BTC | avg duration | profit | loss |\n'
|
||||||
'------- ----------- -------------- '
|
'|:--------|------------:|---------------:|'
|
||||||
'------------------ -------------- -------- ------\n'
|
'-------------------:|---------------:|---------:|-------:|\n'
|
||||||
'BTC_ETH 2 15.00 '
|
'| ETH/BTC | 2 | 15.00 | '
|
||||||
'0.60000000 20.0 2 0\n'
|
'0.60000000 | 20.0 | 2 | 0 |\n'
|
||||||
'TOTAL 2 15.00 '
|
'| TOTAL | 2 | 15.00 | '
|
||||||
'0.60000000 20.0 2 0'
|
'0.60000000 | 20.0 | 2 | 0 |'
|
||||||
)
|
)
|
||||||
|
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.start() method
|
Test Backtesting.start() method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_timeframe(input1, input2):
|
def get_timeframe(input1, input2):
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
||||||
mocker.patch('freqtrade.exchange.get_ticker_history')
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
backtest=MagicMock(),
|
backtest=MagicMock(),
|
||||||
@ -405,7 +422,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
conf['ticker_interval'] = 1
|
conf['ticker_interval'] = 1
|
||||||
conf['live'] = False
|
conf['live'] = False
|
||||||
conf['datadir'] = None
|
conf['datadir'] = None
|
||||||
@ -426,13 +443,49 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
assert log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf) -> None:
|
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.start() method if no data is found
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_timeframe(input1, input2):
|
||||||
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
backtest=MagicMock(),
|
||||||
|
_generate_text_table=MagicMock(return_value='1'),
|
||||||
|
get_timeframe=get_timeframe,
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
conf['ticker_interval'] = "1m"
|
||||||
|
conf['live'] = False
|
||||||
|
conf['datadir'] = None
|
||||||
|
conf['export'] = None
|
||||||
|
conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
|
backtesting = Backtesting(conf)
|
||||||
|
backtesting.start()
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
|
||||||
|
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.backtest() method
|
Test Backtesting.backtest() method
|
||||||
"""
|
"""
|
||||||
backtesting = _BACKTESTING
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -200)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
@ -443,16 +496,19 @@ def test_backtest(default_conf) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf) -> None:
|
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.backtest() method with 1 min ticker
|
Test Backtesting.backtest() method with 1 min ticker
|
||||||
"""
|
"""
|
||||||
backtesting = _BACKTESTING
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
# Run a backtesting for an exiting 5min ticker_interval
|
# Run a backtesting for an exiting 5min ticker_interval
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -200)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
@ -463,17 +519,19 @@ def test_backtest_1min_ticker_interval(default_conf) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_processed() -> None:
|
def test_processed(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.backtest() method with offline data
|
Test Backtesting.backtest() method with offline data
|
||||||
"""
|
"""
|
||||||
backtesting = _BACKTESTING
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
dict_of_tickerrows = load_data_test('raise')
|
dict_of_tickerrows = load_data_test('raise')
|
||||||
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
|
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
|
||||||
dataframe = dataframes['BTC_UNITEST']
|
dataframe = dataframes['UNITTEST/BTC']
|
||||||
cols = dataframe.columns
|
cols = dataframe.columns
|
||||||
# assert the dataframe got some of the indicator columns
|
# assert the dataframe got some of the indicator columns
|
||||||
for col in ['close', 'high', 'low', 'open', 'date',
|
for col in ['close', 'high', 'low', 'open', 'date',
|
||||||
@ -481,76 +539,109 @@ def test_processed() -> None:
|
|||||||
assert col in cols
|
assert col in cols
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
|
|
||||||
# Test backtest using offline data (testdata directory)
|
# Test backtest using offline data (testdata directory)
|
||||||
def test_backtest_ticks(default_conf):
|
def test_backtest_ticks(default_conf, fee, mocker):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
ticks = [1, 5]
|
ticks = [1, 5]
|
||||||
fun = _BACKTESTING.populate_buy_trend
|
fun = Backtesting(default_conf).populate_buy_trend
|
||||||
for tick in ticks:
|
for _ in ticks:
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
results = _run_backtest_1(fun, backtest_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.populate_buy_trend = fun # Override
|
||||||
|
backtesting.populate_sell_trend = fun # Override
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(default_conf):
|
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our DefaultStrategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None):
|
||||||
buy_value = 1
|
buy_value = 1
|
||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
results = _run_backtest_1(fun, backtest_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.populate_buy_trend = fun # Override
|
||||||
|
backtesting.populate_sell_trend = fun # Override
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_only_sell(default_conf):
|
def test_backtest_only_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our DefaultStrategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None):
|
||||||
buy_value = 0
|
buy_value = 0
|
||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
results = _run_backtest_1(fun, backtest_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.populate_buy_trend = fun # Override
|
||||||
|
backtesting.populate_sell_trend = fun # Override
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_alternate_buy_sell(default_conf):
|
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
||||||
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||||
assert len(results) == 3
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.populate_buy_trend = _trend_alternate # Override
|
||||||
|
backtesting.populate_sell_trend = _trend_alternate # Override
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
|
# One trade was force-closed at the end
|
||||||
|
assert len(results.loc[results.open_at_end]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
names = []
|
names = []
|
||||||
records = []
|
records = []
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.optimize.backtesting.file_dump_json',
|
'freqtrade.optimize.backtesting.file_dump_json',
|
||||||
new=lambda n, r: (names.append(n), records.append(r))
|
new=lambda n, r: (names.append(n), records.append(r))
|
||||||
)
|
)
|
||||||
backtest_conf = _make_backtest_conf(
|
|
||||||
conf=default_conf,
|
backtesting = Backtesting(default_conf)
|
||||||
pair='BTC_UNITEST',
|
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
|
||||||
record="trades"
|
"UNITTEST/BTC", "UNITTEST/BTC"],
|
||||||
)
|
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
|
||||||
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
|
||||||
assert len(results) == 3
|
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 21, 36, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 12, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 44, 00).datetime],
|
||||||
|
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
||||||
|
"open_index": [1, 119, 153, 185],
|
||||||
|
"close_index": [118, 151, 184, 199],
|
||||||
|
"trade_duration": [123, 34, 31, 14]})
|
||||||
|
backtesting._store_backtest_result("backtest-result.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
assert names == ['backtest-result.json']
|
assert names == ['backtest-result.json']
|
||||||
records = records[0]
|
records = records[0]
|
||||||
# Ensure records are of correct type
|
# Ensure records are of correct type
|
||||||
assert len(records) == 3
|
assert len(records) == 4
|
||||||
# ('BTC_UNITEST', 0.00331158, '1510684320', '1510691700', 0, 117)
|
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||||
# Below follows just a typecheck of the schema/type of trade-records
|
# Below follows just a typecheck of the schema/type of trade-records
|
||||||
oix = None
|
oix = None
|
||||||
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
|
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
|
||||||
assert pair == 'BTC_UNITEST'
|
assert pair == 'UNITTEST/BTC'
|
||||||
isinstance(profit, float)
|
isinstance(profit, float)
|
||||||
# FIX: buy/sell should be converted to ints
|
# FIX: buy/sell should be converted to ints
|
||||||
isinstance(date_buy, str)
|
isinstance(date_buy, str)
|
||||||
@ -563,13 +654,15 @@ def test_backtest_record(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch('freqtrade.exchange.get_ticker_history',
|
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
new=lambda n, i: _load_pair_as_ticks(n, i))
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
|
||||||
|
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = MagicMock()
|
args = MagicMock()
|
||||||
@ -584,26 +677,29 @@ 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', '1',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--timerange', '-100'
|
'--timerange', '-100',
|
||||||
|
'--realistic-simulation'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start(args)
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
'Parameter -i/--ticker-interval detected ...',
|
'Parameter -i/--ticker-interval detected ...',
|
||||||
'Using ticker_interval: 1 ...',
|
'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 ...',
|
||||||
'Parameter --datadir detected: 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)
|
||||||
|
@ -1,20 +1,30 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, start
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
from freqtrade.tests.conftest import default_conf, log_has
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
# Avoid to reinit the same object again and again
|
||||||
_HYPEROPT = Hyperopt(default_conf())
|
_HYPEROPT_INITIALIZED = False
|
||||||
|
_HYPEROPT = None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def init_hyperopt(default_conf, mocker):
|
||||||
|
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
||||||
|
if not _HYPEROPT_INITIALIZED:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
_HYPEROPT = Hyperopt(default_conf)
|
||||||
|
_HYPEROPT_INITIALIZED = True
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
@ -50,10 +60,13 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
Test start() function
|
Test start() function
|
||||||
"""
|
"""
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
patch_exchange(mocker)
|
||||||
read_data=json.dumps(default_conf)
|
|
||||||
))
|
|
||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
@ -74,7 +87,7 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count() -> None:
|
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.calculate_loss()
|
Test Hyperopt.calculate_loss()
|
||||||
"""
|
"""
|
||||||
@ -88,7 +101,7 @@ def test_loss_calculation_prefer_correct_trade_count() -> None:
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_shorter_trades() -> None:
|
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.calculate_loss()
|
Test Hyperopt.calculate_loss()
|
||||||
"""
|
"""
|
||||||
@ -99,7 +112,7 @@ def test_loss_calculation_prefer_shorter_trades() -> None:
|
|||||||
assert shorter < longer
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_has_limited_profit() -> None:
|
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
|
|
||||||
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||||
@ -109,7 +122,7 @@ def test_loss_calculation_has_limited_profit() -> None:
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_log_results_if_loss_improves(capsys) -> None:
|
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
hyperopt.log_results(
|
hyperopt.log_results(
|
||||||
@ -124,7 +137,7 @@ def test_log_results_if_loss_improves(capsys) -> None:
|
|||||||
assert ' 1/2: foo. Loss 1.00000'in out
|
assert ' 1/2: foo. Loss 1.00000'in out
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(caplog) -> None:
|
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
hyperopt.log_results(
|
hyperopt.log_results(
|
||||||
@ -135,7 +148,7 @@ def test_no_log_if_loss_does_not_improve(caplog) -> None:
|
|||||||
assert caplog.record_tuples == []
|
assert caplog.record_tuples == []
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
|
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||||
fmin_result = {
|
fmin_result = {
|
||||||
"macd_below_zero": 0,
|
"macd_below_zero": 0,
|
||||||
"adx": 1,
|
"adx": 1,
|
||||||
@ -168,7 +181,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
hyperopt = Hyperopt(conf)
|
hyperopt = Hyperopt(conf)
|
||||||
@ -203,7 +216,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
|
|||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
|
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||||
|
|
||||||
@ -212,7 +225,8 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
|
|||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
hyperopt = Hyperopt(conf)
|
hyperopt = Hyperopt(conf)
|
||||||
hyperopt.trials = create_trials(mocker)
|
hyperopt.trials = create_trials(mocker)
|
||||||
@ -230,13 +244,12 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
|
|||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
|
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.update({'config': 'config.json.example'})
|
conf.update({'config': 'config.json.example'})
|
||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'mongodb': False})
|
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
@ -253,7 +266,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
|
|||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
hyperopt = Hyperopt(conf)
|
hyperopt = Hyperopt(conf)
|
||||||
@ -272,7 +285,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
|
|||||||
assert total_tries == (current_tries + len(trials.results))
|
assert total_tries == (current_tries + len(trials.results))
|
||||||
|
|
||||||
|
|
||||||
def test_save_trials_saves_trials(mocker, caplog) -> None:
|
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
|
||||||
create_trials(mocker)
|
create_trials(mocker)
|
||||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||||
|
|
||||||
@ -281,22 +294,24 @@ def test_save_trials_saves_trials(mocker, caplog) -> None:
|
|||||||
|
|
||||||
hyperopt.save_trials()
|
hyperopt.save_trials()
|
||||||
|
|
||||||
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
'Saving Trials to \'{}\''.format(trials_file),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
mock_dump.assert_called_once()
|
mock_dump.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
|
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
||||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
||||||
|
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
hyperopt_trial = hyperopt.read_trials()
|
hyperopt_trial = hyperopt.read_trials()
|
||||||
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
'Reading Trials from \'{}\''.format(trials_file),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert hyperopt_trial == trials
|
assert hyperopt_trial == trials
|
||||||
@ -304,7 +319,7 @@ def test_read_trials_returns_trials_file(mocker, caplog) -> None:
|
|||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_roi_table_generation() -> None:
|
def test_roi_table_generation(init_hyperopt) -> None:
|
||||||
params = {
|
params = {
|
||||||
'roi_t1': 5,
|
'roi_t1': 5,
|
||||||
'roi_t2': 10,
|
'roi_t2': 10,
|
||||||
@ -318,16 +333,16 @@ def test_roi_table_generation() -> None:
|
|||||||
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_fmin(mocker, default_conf) -> None:
|
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
patch_exchange(mocker)
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.update({'config': 'config.json.example'})
|
conf.update({'config': 'config.json.example'})
|
||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'mongodb': False})
|
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
@ -339,50 +354,37 @@ def test_start_calls_fmin(mocker, default_conf) -> None:
|
|||||||
mock_fmin.assert_called_once()
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_start_uses_mongotrials(mocker, default_conf) -> None:
|
def test_format_results(init_hyperopt):
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
|
||||||
mock_mongotrials = mocker.patch(
|
|
||||||
'freqtrade.optimize.hyperopt.MongoTrials',
|
|
||||||
return_value=create_trials(mocker)
|
|
||||||
)
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'epochs': 1})
|
|
||||||
conf.update({'mongodb': True})
|
|
||||||
conf.update({'timerange': None})
|
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
|
|
||||||
hyperopt = Hyperopt(conf)
|
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
|
||||||
|
|
||||||
hyperopt.start()
|
|
||||||
mock_mongotrials.assert_called_once()
|
|
||||||
mock_fmin.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
# test log_trials_result
|
|
||||||
# test buy_strategy_generator def populate_buy_trend
|
|
||||||
# test optimizer if 'ro_t1' in params
|
|
||||||
|
|
||||||
def test_format_results():
|
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.format_results()
|
Test Hyperopt.format_results()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Test with BTC as stake_currency
|
||||||
trades = [
|
trades = [
|
||||||
('BTC_ETH', 2, 2, 123),
|
('ETH/BTC', 2, 2, 123),
|
||||||
('BTC_LTC', 1, 1, 123),
|
('LTC/BTC', 1, 1, 123),
|
||||||
('BTC_XRP', -1, -2, -246)
|
('XPR/BTC', -1, -2, -246)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
x = Hyperopt.format_results(df)
|
|
||||||
assert x.find(' 66.67%')
|
result = _HYPEROPT.format_results(df)
|
||||||
|
assert result.find(' 66.67%')
|
||||||
|
assert result.find('Total profit 1.00000000 BTC')
|
||||||
|
assert result.find('2.0000Σ %')
|
||||||
|
|
||||||
|
# Test with EUR as stake_currency
|
||||||
|
trades = [
|
||||||
|
('ETH/EUR', 2, 2, 123),
|
||||||
|
('LTC/EUR', 1, 1, 123),
|
||||||
|
('XPR/EUR', -1, -2, -246)
|
||||||
|
]
|
||||||
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
result = _HYPEROPT.format_results(df)
|
||||||
|
assert result.find('Total profit 1.00000000 EUR')
|
||||||
|
|
||||||
|
|
||||||
def test_signal_handler(mocker):
|
def test_signal_handler(mocker, init_hyperopt):
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.signal_handler()
|
Test Hyperopt.signal_handler()
|
||||||
"""
|
"""
|
||||||
@ -392,11 +394,11 @@ def test_signal_handler(mocker):
|
|||||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
||||||
|
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
hyperopt.signal_handler(9, None)
|
hyperopt.signal_handler(signal.SIGTERM, None)
|
||||||
assert m.call_count == 3
|
assert m.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
def test_has_space():
|
def test_has_space(init_hyperopt):
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.has_space() method
|
Test Hyperopt.has_space() method
|
||||||
"""
|
"""
|
||||||
@ -409,14 +411,14 @@ def test_has_space():
|
|||||||
assert _HYPEROPT.has_space('buy')
|
assert _HYPEROPT.has_space('buy')
|
||||||
|
|
||||||
|
|
||||||
def test_populate_indicators() -> None:
|
def test_populate_indicators(init_hyperopt) -> None:
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.populate_indicators()
|
Test Hyperopt.populate_indicators()
|
||||||
"""
|
"""
|
||||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||||
|
|
||||||
# Check if some indicators are generated. We will not test all of them
|
# Check if some indicators are generated. We will not test all of them
|
||||||
assert 'adx' in dataframe
|
assert 'adx' in dataframe
|
||||||
@ -424,14 +426,14 @@ def test_populate_indicators() -> None:
|
|||||||
assert 'cci' in dataframe
|
assert 'cci' in dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_buy_strategy_generator() -> None:
|
def test_buy_strategy_generator(init_hyperopt) -> None:
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.buy_strategy_generator()
|
Test Hyperopt.buy_strategy_generator()
|
||||||
"""
|
"""
|
||||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
||||||
|
|
||||||
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
||||||
{
|
{
|
||||||
@ -481,7 +483,7 @@ def test_buy_strategy_generator() -> None:
|
|||||||
assert 1 in result['buy']
|
assert 1 in result['buy']
|
||||||
|
|
||||||
|
|
||||||
def test_generate_optimizer(mocker, default_conf) -> None:
|
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.generate_optimizer() function
|
Test Hyperopt.generate_optimizer() function
|
||||||
"""
|
"""
|
||||||
@ -491,15 +493,16 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
|||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
trades = [
|
trades = [
|
||||||
('BTC_POWR', 0.023117, 0.000233, 100)
|
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||||
MagicMock(return_value=backtest_result)
|
MagicMock(return_value=backtest_result)
|
||||||
)
|
)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
optimizer_param = {
|
optimizer_param = {
|
||||||
'adx': {'enabled': False},
|
'adx': {'enabled': False},
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
|
||||||
|
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_optimize_conf():
|
|
||||||
hyperopt_conf = hyperopt_optimize_conf()
|
|
||||||
|
|
||||||
assert "max_open_trades" in hyperopt_conf
|
|
||||||
assert "stake_currency" in hyperopt_conf
|
|
||||||
assert "stake_amount" in hyperopt_conf
|
|
||||||
assert "minimal_roi" in hyperopt_conf
|
|
||||||
assert "stoploss" in hyperopt_conf
|
|
||||||
assert "bid_strategy" in hyperopt_conf
|
|
||||||
assert "exchange" in hyperopt_conf
|
|
||||||
assert "pair_whitelist" in hyperopt_conf['exchange']
|
|
@ -3,15 +3,18 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
import arrow
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
from freqtrade import optimize
|
from freqtrade import optimize
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
|
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
||||||
from freqtrade.tests.conftest import log_has
|
load_cached_data_for_updating
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||||
|
|
||||||
# Change this if modifying BTC_UNITEST testdatafile
|
# Change this if modifying UNITTEST/BTC testdatafile
|
||||||
_BTC_UNITTEST_LENGTH = 13681
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
|
|
||||||
|
|
||||||
@ -46,31 +49,30 @@ def _clean_test_file(file: str) -> None:
|
|||||||
os.rename(file_swp, file)
|
os.rename(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
|
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test load_data() with 30 min ticker
|
Test load_data() with 30 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
||||||
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
|
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
|
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert not log_has('Download the pair: "BTC_ETH", Interval: 30 min', caplog.record_tuples)
|
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
|
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test load_data() with 5 min ticker
|
Test load_data() with 5 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
|
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='5m')
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert not log_has('Download the pair: "BTC_ETH", Interval: 5 min', caplog.record_tuples)
|
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 5m', caplog.record_tuples)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
@ -78,27 +80,43 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
|||||||
"""
|
"""
|
||||||
Test load_data() with 1 min ticker
|
Test load_data() with 1 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
|
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert not log_has('Download the pair: "BTC_ETH", Interval: 1 min', caplog.record_tuples)
|
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
|
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test load_data() with 1 min ticker
|
Test load_data() with 1 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
|
||||||
_backup_file(file)
|
_backup_file(file)
|
||||||
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
|
# do not download a new pair if refresh_pairs isn't set
|
||||||
|
optimize.load_data(None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=False,
|
||||||
|
pairs=['MEME/BTC'])
|
||||||
|
assert os.path.isfile(file) is False
|
||||||
|
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
|
||||||
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
# download a new pair if refresh_pairs is set
|
||||||
|
optimize.load_data(None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=True,
|
||||||
|
exchange=exchange,
|
||||||
|
pairs=['MEME/BTC'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert log_has('Download the pair: "BTC_MEME", Interval: 1 min', caplog.record_tuples)
|
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
@ -106,13 +124,13 @@ def test_testdata_path() -> None:
|
|||||||
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs(ticker_history, mocker) -> None:
|
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
|
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
|
||||||
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
|
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
|
||||||
|
|
||||||
_backup_file(file1_1)
|
_backup_file(file1_1)
|
||||||
_backup_file(file1_5)
|
_backup_file(file1_5)
|
||||||
@ -122,7 +140,8 @@ def test_download_pairs(ticker_history, mocker) -> None:
|
|||||||
assert os.path.isfile(file1_1) is False
|
assert os.path.isfile(file1_1) is False
|
||||||
assert os.path.isfile(file2_1) is False
|
assert os.path.isfile(file2_1) is False
|
||||||
|
|
||||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=1) is True
|
assert download_pairs(None, exchange,
|
||||||
|
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True
|
||||||
|
|
||||||
assert os.path.isfile(file1_1) is True
|
assert os.path.isfile(file1_1) is True
|
||||||
assert os.path.isfile(file2_1) is True
|
assert os.path.isfile(file2_1) is True
|
||||||
@ -134,7 +153,8 @@ def test_download_pairs(ticker_history, mocker) -> None:
|
|||||||
assert os.path.isfile(file1_5) is False
|
assert os.path.isfile(file1_5) is False
|
||||||
assert os.path.isfile(file2_5) is False
|
assert os.path.isfile(file2_5) is False
|
||||||
|
|
||||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=5) is True
|
assert download_pairs(None, exchange,
|
||||||
|
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True
|
||||||
|
|
||||||
assert os.path.isfile(file1_5) is True
|
assert os.path.isfile(file1_5) is True
|
||||||
assert os.path.isfile(file2_5) is True
|
assert os.path.isfile(file2_5) is True
|
||||||
@ -144,91 +164,201 @@ def test_download_pairs(ticker_history, mocker) -> None:
|
|||||||
_clean_test_file(file2_5)
|
_clean_test_file(file2_5)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
|
def test_load_cached_data_for_updating(mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
datadir = os.path.join(os.path.dirname(__file__), '..', 'testdata')
|
||||||
|
|
||||||
|
test_data = None
|
||||||
|
test_filename = os.path.join(datadir, 'UNITTEST_BTC-1m.json')
|
||||||
|
with open(test_filename, "rt") as file:
|
||||||
|
test_data = json.load(file)
|
||||||
|
|
||||||
|
# change now time to test 'line' cases
|
||||||
|
# now = last cached item + 1 hour
|
||||||
|
now_ts = test_data[-1][0] / 1000 + 60 * 60
|
||||||
|
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
|
||||||
|
|
||||||
|
# timeframe starts earlier than the cached data
|
||||||
|
# should fully update data
|
||||||
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == []
|
||||||
|
assert start_ts == test_data[0][0] - 1000
|
||||||
|
|
||||||
|
# same with 'line' timeframe
|
||||||
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
TimeRange(None, 'line', 0, -num_lines))
|
||||||
|
assert data == []
|
||||||
|
assert start_ts < test_data[0][0] - 1
|
||||||
|
|
||||||
|
# timeframe starts in the center of the cached data
|
||||||
|
# should return the chached data w/o the last item
|
||||||
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == test_data[:-1]
|
||||||
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
|
# same with 'line' timeframe
|
||||||
|
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
|
||||||
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == test_data[:-1]
|
||||||
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
|
# timeframe starts after the chached data
|
||||||
|
# should return the chached data w/o the last item
|
||||||
|
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == test_data[:-1]
|
||||||
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
|
# same with 'line' timeframe
|
||||||
|
num_lines = 30
|
||||||
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == test_data[:-1]
|
||||||
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
|
# no timeframe is set
|
||||||
|
# should return the chached data w/o the last item
|
||||||
|
num_lines = 30
|
||||||
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename,
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == test_data[:-1]
|
||||||
|
assert test_data[-2][0] < start_ts < test_data[-1][0]
|
||||||
|
|
||||||
|
# no datafile exist
|
||||||
|
# should return timestamp start time
|
||||||
|
timerange = TimeRange('date', None, now_ts - 10000, 0)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == []
|
||||||
|
assert start_ts == (now_ts - 10000) * 1000
|
||||||
|
|
||||||
|
# same with 'line' timeframe
|
||||||
|
num_lines = 30
|
||||||
|
timerange = TimeRange(None, 'line', 0, -num_lines)
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||||
|
'1m',
|
||||||
|
timerange)
|
||||||
|
assert data == []
|
||||||
|
assert start_ts == (now_ts - num_lines * 60) * 1000
|
||||||
|
|
||||||
|
# no datafile exist, no timeframe is set
|
||||||
|
# should return an empty array and None
|
||||||
|
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
|
||||||
|
'1m',
|
||||||
|
None)
|
||||||
|
assert data == []
|
||||||
|
assert start_ts is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=BaseException('File Error'))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
_backup_file(file1_1)
|
_backup_file(file1_1)
|
||||||
_backup_file(file1_5)
|
_backup_file(file1_5)
|
||||||
|
|
||||||
download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
|
download_pairs(None, exchange, pairs=['MEME/BTC'], ticker_interval='1m')
|
||||||
# clean files freshly downloaded
|
# clean files freshly downloaded
|
||||||
_clean_test_file(file1_1)
|
_clean_test_file(file1_1)
|
||||||
_clean_test_file(file1_5)
|
_clean_test_file(file1_5)
|
||||||
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
|
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata(ticker_history, mocker) -> None:
|
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
# Download a 1 min ticker file
|
||||||
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json')
|
||||||
_backup_file(file1)
|
_backup_file(file1)
|
||||||
download_backtesting_testdata(None, pair="BTC-XEL", interval=1)
|
download_backtesting_testdata(None, exchange, pair="XEL/BTC", tick_interval='1m')
|
||||||
assert os.path.isfile(file1) is True
|
assert os.path.isfile(file1) is True
|
||||||
_clean_test_file(file1)
|
_clean_test_file(file1)
|
||||||
|
|
||||||
# Download a 5 min ticker file
|
# Download a 5 min ticker file
|
||||||
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
|
file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json')
|
||||||
_backup_file(file2)
|
_backup_file(file2)
|
||||||
|
|
||||||
download_backtesting_testdata(None, pair="BTC-STORJ", interval=5)
|
download_backtesting_testdata(None, exchange, pair="STORJ/BTC", tick_interval='5m')
|
||||||
assert os.path.isfile(file2) is True
|
assert os.path.isfile(file2) is True
|
||||||
_clean_test_file(file2)
|
_clean_test_file(file2)
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata2(mocker) -> None:
|
def test_download_backtesting_testdata2(mocker, default_conf) -> None:
|
||||||
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
tick = [
|
||||||
|
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
|
||||||
|
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||||
|
]
|
||||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick)
|
||||||
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
|
||||||
|
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
||||||
assert json_dump_mock.call_count == 2
|
assert json_dump_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_load_tickerdata_file() -> None:
|
def test_load_tickerdata_file() -> None:
|
||||||
# 7 does not exist in either format.
|
# 7 does not exist in either format.
|
||||||
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
|
||||||
# 1 exists only as a .json
|
# 1 exists only as a .json
|
||||||
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||||
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 8)
|
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
|
||||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker) -> None:
|
def test_init(default_conf, mocker) -> None:
|
||||||
conf = {'exchange': {'pair_whitelist': []}}
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
assert {} == optimize.load_data(
|
assert {} == optimize.load_data(
|
||||||
'',
|
'',
|
||||||
|
exchange=exchange,
|
||||||
pairs=[],
|
pairs=[],
|
||||||
refresh_pairs=True,
|
refresh_pairs=True,
|
||||||
ticker_interval=int(default_conf['ticker_interval'])
|
ticker_interval=default_conf['ticker_interval']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_trim_tickerlist() -> None:
|
def test_trim_tickerlist() -> None:
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
|
with open(file) as data_file:
|
||||||
ticker_list = json.load(data_file)
|
ticker_list = json.load(data_file)
|
||||||
ticker_list_len = len(ticker_list)
|
ticker_list_len = len(ticker_list)
|
||||||
|
|
||||||
# Test the pattern ^(-\d+)$
|
# Test the pattern ^(-\d+)$
|
||||||
# This pattern remove X element from the beginning
|
# This pattern uses the latest N elements
|
||||||
timerange = ((None, 'line'), None, 5)
|
timerange = TimeRange(None, 'line', 0, -5)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
assert ticker_list_len == ticker_len + 5
|
assert ticker_len == 5
|
||||||
assert ticker_list[0] is not ticker[0] # The first element should be different
|
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||||
assert ticker_list[-1] is ticker[-1] # The last element must be the same
|
assert ticker_list[-1] is ticker[-1] # The last element must be the same
|
||||||
|
|
||||||
# Test the pattern ^(\d+)-$
|
# Test the pattern ^(\d+)-$
|
||||||
# This pattern keep X element from the end
|
# This pattern keep X element from the end
|
||||||
timerange = (('line', None), 5, None)
|
timerange = TimeRange('line', None, 5, 0)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -238,7 +368,7 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
# Test the pattern ^(\d+)-(\d+)$
|
# Test the pattern ^(\d+)-(\d+)$
|
||||||
# This pattern extract a window
|
# This pattern extract a window
|
||||||
timerange = (('index', 'index'), 5, 10)
|
timerange = TimeRange('index', 'index', 5, 10)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -247,9 +377,40 @@ def test_trim_tickerlist() -> None:
|
|||||||
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
||||||
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
||||||
|
|
||||||
|
# Test the pattern ^(\d{8})-(\d{8})$
|
||||||
|
# This pattern extract a window between the dates
|
||||||
|
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == 5
|
||||||
|
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||||
|
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
||||||
|
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
||||||
|
|
||||||
|
# Test the pattern ^-(\d{8})$
|
||||||
|
# This pattern extracts elements from the start to the date
|
||||||
|
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == 10
|
||||||
|
assert ticker_list[0] is ticker[0] # The start of the list is included
|
||||||
|
assert ticker_list[9] is ticker[-1] # The element 10 is not included
|
||||||
|
|
||||||
|
# Test the pattern ^(\d{8})-$
|
||||||
|
# This pattern extracts elements from the date to now
|
||||||
|
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == ticker_list_len - 10
|
||||||
|
assert ticker_list[10] is ticker[0] # The first element is element #10
|
||||||
|
assert ticker_list[-1] is ticker[-1] # The last element is the same
|
||||||
|
|
||||||
# Test a wrong pattern
|
# Test a wrong pattern
|
||||||
# This pattern must return the list unchanged
|
# This pattern must return the list unchanged
|
||||||
timerange = ((None, None), None, 5)
|
timerange = TimeRange(None, None, None, 5)
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
ticker_len = len(ticker)
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
@ -261,7 +422,8 @@ def test_file_dump_json() -> None:
|
|||||||
Test file_dump_json()
|
Test file_dump_json()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||||
|
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||||
data = {'bar': 'foo'}
|
data = {'bar': 'foo'}
|
||||||
|
|
||||||
# check the file we will create does not exist
|
# check the file we will create does not exist
|
||||||
|
@ -7,11 +7,11 @@ Unit test file for rpc/rpc.py
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
import pytest
|
||||||
|
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||||
|
|
||||||
@ -25,41 +25,40 @@ def prec_satoshi(a, b) -> float:
|
|||||||
|
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
|
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_trade_status() method
|
Test rpc_trade_status() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, result) = rpc.rpc_trade_status()
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
assert error
|
rpc._rpc_trade_status()
|
||||||
assert 'trader is not running' in result
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, result) = rpc.rpc_trade_status()
|
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||||
assert error
|
rpc._rpc_trade_status()
|
||||||
assert 'no active trade' in result
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, result) = rpc.rpc_trade_status()
|
trades = rpc._rpc_trade_status()
|
||||||
assert not error
|
trade = trades[0]
|
||||||
trade = result[0]
|
|
||||||
|
|
||||||
result_message = [
|
result_message = [
|
||||||
'*Trade ID:* `1`\n'
|
'*Trade ID:* `1`\n'
|
||||||
'*Current Pair:* '
|
'*Current Pair:* '
|
||||||
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
||||||
'*Open Since:* `just now`\n'
|
'*Open Since:* `just now`\n'
|
||||||
'*Amount:* `90.99181074`\n'
|
'*Amount:* `90.99181074`\n'
|
||||||
'*Open Rate:* `0.00001099`\n'
|
'*Open Rate:* `0.00001099`\n'
|
||||||
@ -67,60 +66,62 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
|
|||||||
'*Current Rate:* `0.00001098`\n'
|
'*Current Rate:* `0.00001098`\n'
|
||||||
'*Close Profit:* `None`\n'
|
'*Close Profit:* `None`\n'
|
||||||
'*Current Profit:* `-0.59%`\n'
|
'*Current Profit:* `-0.59%`\n'
|
||||||
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
|
'*Open Order:* `(limit buy rem=0.00000000)`'
|
||||||
]
|
]
|
||||||
assert result == result_message
|
assert trades == result_message
|
||||||
assert trade.find('[BTC_ETH]') >= 0
|
assert trade.find('[ETH/BTC]') >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_status_table() method
|
Test rpc_status_table() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, result) = rpc.rpc_status_table()
|
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
|
||||||
assert error
|
rpc._rpc_status_table()
|
||||||
assert '*Status:* `trader is not running`' in result
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, result) = rpc.rpc_status_table()
|
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
|
||||||
assert error
|
rpc._rpc_status_table()
|
||||||
assert '*Status:* `no active order`' in result
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, result) = rpc.rpc_status_table()
|
result = rpc._rpc_status_table()
|
||||||
assert 'just now' in result['Since'].all()
|
assert 'just now' in result['Since'].all()
|
||||||
assert 'BTC_ETH' in result['Pair'].all()
|
assert 'ETH/BTC' in result['Pair'].all()
|
||||||
assert '-0.59%' in result['Profit'].all()
|
assert '-0.59%' in result['Profit'].all()
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
|
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
-> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_daily_profit() method
|
Test rpc_daily_profit() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -139,8 +140,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
|||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
update.message.text = '/daily 2'
|
||||||
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||||
assert not error
|
|
||||||
assert len(days) == 7
|
assert len(days) == 7
|
||||||
for day in days:
|
for day in days:
|
||||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||||
@ -153,13 +153,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
|
|||||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||||
assert error
|
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||||
assert days.find('must be an integer greater than 0') >= 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_trade_statistics() method
|
Test rpc_trade_statistics() method
|
||||||
"""
|
"""
|
||||||
@ -169,22 +168,23 @@ def test_rpc_trade_statistics(
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
||||||
assert error
|
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert stats.find('no closed trade') >= 0
|
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
@ -194,7 +194,7 @@ def test_rpc_trade_statistics(
|
|||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
@ -202,26 +202,40 @@ def test_rpc_trade_statistics(
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
freqtradebot.create_trade()
|
||||||
assert not error
|
trade = Trade.query.first()
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
# Update the ticker with a market going up
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||||
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
|
||||||
assert prec_satoshi(stats['profit_all_percent'], 6.2)
|
assert prec_satoshi(stats['profit_all_percent'], 2.81)
|
||||||
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
|
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
|
||||||
assert stats['trade_count'] == 1
|
assert stats['trade_count'] == 2
|
||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == 'just now'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
assert stats['avg_duration'] == '0:00:00'
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
assert stats['best_pair'] == 'BTC_ETH'
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
# Test that rpc_trade_statistics can handle trades that lacks
|
# Test that rpc_trade_statistics can handle trades that lacks
|
||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
|
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
limit_sell_order):
|
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||||
"""
|
"""
|
||||||
Test rpc_trade_statistics() method
|
Test rpc_trade_statistics() method
|
||||||
"""
|
"""
|
||||||
@ -231,14 +245,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -251,9 +267,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
|
|||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up,
|
||||||
|
get_fee=fee
|
||||||
)
|
)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
@ -262,8 +279,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
|
|||||||
for trade in Trade.query.order_by(Trade.id).all():
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
trade.open_rate = None
|
trade.open_rate = None
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert not error
|
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
||||||
@ -274,7 +290,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
|
|||||||
assert stats['first_trade_date'] == 'just now'
|
assert stats['first_trade_date'] == 'just now'
|
||||||
assert stats['latest_trade_date'] == 'just now'
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
assert stats['avg_duration'] == '0:00:00'
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
assert stats['best_pair'] == 'BTC_ETH'
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
@ -282,22 +298,18 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
"""
|
"""
|
||||||
Test rpc_balance() method
|
Test rpc_balance() method
|
||||||
"""
|
"""
|
||||||
mock_balance = [
|
mock_balance = {
|
||||||
{
|
'BTC': {
|
||||||
'Currency': 'BTC',
|
'free': 10.0,
|
||||||
'Balance': 10.0,
|
'total': 12.0,
|
||||||
'Available': 12.0,
|
'used': 2.0,
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
},
|
},
|
||||||
{
|
'ETH': {
|
||||||
'Currency': 'ETH',
|
'free': 0.0,
|
||||||
'Balance': 0.0,
|
'total': 0.0,
|
||||||
'Available': 0.0,
|
'used': 0.0,
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -305,28 +317,26 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_balances=MagicMock(return_value=mock_balance)
|
get_balances=MagicMock(return_value=mock_balance)
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||||
assert not error
|
assert prec_satoshi(total, 12)
|
||||||
(trade, x, y, z) = res
|
assert prec_satoshi(value, 180000)
|
||||||
assert prec_satoshi(x, 10)
|
assert 'USD' in symbol
|
||||||
assert prec_satoshi(z, 150000)
|
assert len(output) == 1
|
||||||
assert 'USD' in y
|
assert 'BTC' in output[0]['currency']
|
||||||
assert len(trade) == 1
|
assert prec_satoshi(output[0]['available'], 10)
|
||||||
assert 'BTC' in trade[0]['currency']
|
assert prec_satoshi(output[0]['balance'], 12)
|
||||||
assert prec_satoshi(trade[0]['available'], 12)
|
assert prec_satoshi(output[0]['pending'], 2)
|
||||||
assert prec_satoshi(trade[0]['balance'], 10)
|
assert prec_satoshi(output[0]['est_btc'], 12)
|
||||||
assert prec_satoshi(trade[0]['pending'], 0)
|
|
||||||
assert prec_satoshi(trade[0]['est_btc'], 10)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_start(mocker, default_conf) -> None:
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
@ -335,24 +345,22 @@ def test_rpc_start(mocker, default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
|
||||||
(error, result) = rpc.rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert not error
|
|
||||||
assert '`Starting trader ...`' in result
|
assert '`Starting trader ...`' in result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
(error, result) = rpc.rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert error
|
|
||||||
assert '*Status:* `already running`' in result
|
assert '*Status:* `already running`' in result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
@ -363,134 +371,147 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
(error, result) = rpc.rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert not error
|
|
||||||
assert '`Stopping trader ...`' in result
|
assert '`Stopping trader ...`' in result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
(error, result) = rpc.rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert error
|
|
||||||
assert '*Status:* `already stopped`' in result
|
assert '*Status:* `already stopped`' in result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
|
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_forcesell() method
|
Test rpc_forcesell() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
get_order=MagicMock(
|
get_order=MagicMock(
|
||||||
return_value={
|
return_value={
|
||||||
'closed': True,
|
'status': 'closed',
|
||||||
'type': 'LIMIT_BUY',
|
'type': 'limit',
|
||||||
|
'side': 'buy'
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == 'Invalid argument.'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
rpc._rpc_forcesell('1')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell('all')
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
# make an limit-buy open trade
|
# make an limit-buy open trade
|
||||||
|
trade = Trade.query.filter(Trade.id == '1').first()
|
||||||
|
filled_amount = trade.amount / 2
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.freqtradebot.exchange.get_order',
|
'freqtrade.exchange.Exchange.get_order',
|
||||||
return_value={
|
return_value={
|
||||||
'closed': None,
|
'status': 'open',
|
||||||
'type': 'LIMIT_BUY'
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'filled': filled_amount
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# check that the trade is called, which is done
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
# by ensuring exchange.cancel_order is called
|
# and trade amount is updated
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
rpc._rpc_forcesell('1')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
assert trade.amount == filled_amount
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.filter(Trade.id == '2').first()
|
||||||
|
amount = trade.amount
|
||||||
|
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'status': 'open',
|
||||||
|
'type': 'limit',
|
||||||
|
'side': 'buy',
|
||||||
|
'filled': None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
|
rpc._rpc_forcesell('2')
|
||||||
|
assert cancel_order_mock.call_count == 2
|
||||||
|
assert trade.amount == amount
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
# make an limit-sell open trade
|
# make an limit-sell open trade
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.freqtradebot.exchange.get_order',
|
'freqtrade.exchange.Exchange.get_order',
|
||||||
return_value={
|
return_value={
|
||||||
'closed': None,
|
'status': 'open',
|
||||||
'type': 'LIMIT_SELL'
|
'type': 'limit',
|
||||||
|
'side': 'sell'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(error, res) = rpc.rpc_forcesell('2')
|
rpc._rpc_forcesell('3')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
# status quo, no exchange calls
|
# status quo, no exchange calls
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order,
|
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_performance() method
|
Test rpc_performance() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_balances=MagicMock(return_value=ticker),
|
get_balances=MagicMock(return_value=ticker),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -506,39 +527,38 @@ def test_performance_handle(default_conf, ticker, limit_buy_order,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
(error, res) = rpc.rpc_performance()
|
res = rpc._rpc_performance()
|
||||||
assert not error
|
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['pair'] == 'BTC_ETH'
|
assert res[0]['pair'] == 'ETH/BTC'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
assert prec_satoshi(res[0]['profit'], 6.2)
|
assert prec_satoshi(res[0]['profit'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_count(mocker, default_conf, ticker) -> None:
|
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||||
"""
|
"""
|
||||||
Test rpc_count() method
|
Test rpc_count() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_balances=MagicMock(return_value=ticker),
|
get_balances=MagicMock(return_value=ticker),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, trades) = rpc.rpc_count()
|
trades = rpc._rpc_count()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
assert not error
|
|
||||||
assert nb_trades == 0
|
assert nb_trades == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, trades) = rpc.rpc_count()
|
trades = rpc._rpc_count()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
assert not error
|
|
||||||
assert nb_trades == 1
|
assert nb_trades == 1
|
||||||
|
@ -7,49 +7,35 @@ from copy import deepcopy
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.rpc.rpc_manager import RPCManager
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
from freqtrade.rpc.telegram import Telegram
|
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_manager_object() -> None:
|
def test_rpc_manager_object() -> None:
|
||||||
"""
|
""" Test the Arguments object has the mandatory methods """
|
||||||
Test the Arguments object has the mandatory methods
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
assert hasattr(RPCManager, '_init')
|
|
||||||
assert hasattr(RPCManager, 'send_msg')
|
assert hasattr(RPCManager, 'send_msg')
|
||||||
assert hasattr(RPCManager, 'cleanup')
|
assert hasattr(RPCManager, 'cleanup')
|
||||||
|
|
||||||
|
|
||||||
def test__init__(mocker, default_conf) -> None:
|
def test__init__(mocker, default_conf) -> None:
|
||||||
"""
|
""" Test __init__() method """
|
||||||
Test __init__() method
|
conf = deepcopy(default_conf)
|
||||||
"""
|
conf['telegram']['enabled'] = False
|
||||||
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||||
assert rpc_manager.freqtrade == freqtradebot
|
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
assert rpc_manager.telegram is None
|
|
||||||
assert init_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
""" Test _init() method with Telegram disabled """
|
||||||
Test _init() method with Telegram disabled
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
assert rpc_manager.telegram is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
|
||||||
|
|
||||||
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
len_modules = len(rpc_manager.registered_modules)
|
len_modules = len(rpc_manager.registered_modules)
|
||||||
assert len_modules == 1
|
assert len_modules == 1
|
||||||
assert 'telegram' in rpc_manager.registered_modules
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert isinstance(rpc_manager.telegram, Telegram)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
# Check we have Telegram as a registered modules
|
# Check we have Telegram as a registered modules
|
||||||
assert 'telegram' in rpc_manager.registered_modules
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
rpc_manager.cleanup()
|
rpc_manager.cleanup()
|
||||||
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
assert 'telegram' not in rpc_manager.registered_modules
|
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
assert log_has('test', caplog.record_tuples)
|
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
assert log_has('test', caplog.record_tuples)
|
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
@ -11,7 +11,6 @@ from datetime import datetime
|
|||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from telegram import Update, Message, Chat
|
from telegram import Update, Message, Chat
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ from freqtrade.persistence import Trade
|
|||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
from freqtrade.rpc.telegram import authorized_only
|
from freqtrade.rpc.telegram import authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
@ -33,6 +32,9 @@ class DummyCls(Telegram):
|
|||||||
super().__init__(freqtrade)
|
super().__init__(freqtrade)
|
||||||
self.state = {'called': False}
|
self.state = {'called': False}
|
||||||
|
|
||||||
|
def _init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def dummy_handler(self, *args, **kwargs) -> None:
|
def dummy_handler(self, *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
@ -61,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog) -> None:
|
def test_init(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
""" Test _init() method """
|
||||||
Test _init() method
|
|
||||||
"""
|
|
||||||
start_polling = MagicMock()
|
start_polling = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||||
|
|
||||||
@ -71,31 +71,16 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
def test_init_disabled(default_conf, mocker, caplog) -> None:
|
|
||||||
"""
|
|
||||||
Test _init() method when Telegram is disabled
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
|
||||||
"['count'], ['help'], ['version']]"
|
|
||||||
|
|
||||||
assert not log_has(message_str, caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup(default_conf, mocker) -> None:
|
def test_cleanup(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test cleanup() method
|
Test cleanup() method
|
||||||
@ -104,51 +89,18 @@ def test_cleanup(default_conf, mocker) -> None:
|
|||||||
updater_mock.stop = MagicMock()
|
updater_mock.stop = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
||||||
|
|
||||||
# not enabled
|
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
telegram.cleanup()
|
|
||||||
assert telegram._updater is None
|
|
||||||
assert updater_mock.call_count == 0
|
|
||||||
assert not hasattr(telegram._updater, 'stop')
|
|
||||||
assert updater_mock.stop.call_count == 0
|
|
||||||
|
|
||||||
# enabled
|
|
||||||
conf['telegram']['enabled'] = True
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
telegram.cleanup()
|
telegram.cleanup()
|
||||||
assert telegram._updater.stop.call_count == 1
|
assert telegram._updater.stop.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker) -> None:
|
|
||||||
"""
|
|
||||||
Test is_enabled() method
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
|
||||||
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
|
|
||||||
assert telegram.is_enabled()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_not_enabled(default_conf, mocker) -> None:
|
|
||||||
"""
|
|
||||||
Test is_enabled() method
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
|
|
||||||
assert not telegram.is_enabled()
|
|
||||||
|
|
||||||
|
|
||||||
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test authorized_only() method when we are authorized
|
Test authorized_only() method when we are authorized
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
patch_exchange(mocker, None)
|
||||||
|
|
||||||
chat = Chat(0, 0)
|
chat = Chat(0, 0)
|
||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
@ -156,7 +108,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is True
|
assert dummy.state['called'] is True
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -179,15 +131,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
patch_exchange(mocker, None)
|
||||||
|
|
||||||
chat = Chat(0xdeadbeef, 0)
|
chat = Chat(0xdeadbeef, 0)
|
||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -210,14 +161,14 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
patch_exchange(mocker)
|
||||||
|
|
||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
|
dummy = DummyCls(FreqtradeBot(conf))
|
||||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -234,7 +185,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_status(default_conf, update, mocker, ticker) -> None:
|
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
"""
|
"""
|
||||||
Test _status() method
|
Test _status() method
|
||||||
"""
|
"""
|
||||||
@ -246,22 +197,25 @@ def test_status(default_conf, update, mocker, ticker) -> None:
|
|||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_pair_detail_url=MagicMock(),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
status_table = MagicMock()
|
status_table = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])),
|
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -277,16 +231,18 @@ def test_status(default_conf, update, mocker, ticker) -> None:
|
|||||||
assert status_table.call_count == 1
|
assert status_table.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, mocker) -> None:
|
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _status() method
|
Test _status() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
status_table = MagicMock()
|
status_table = MagicMock()
|
||||||
@ -294,11 +250,11 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
|
|||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -319,32 +275,34 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
|
|||||||
telegram._status(bot=MagicMock(), update=update)
|
telegram._status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
|
assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
|
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _status_table() method
|
Test _status_table() method
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_order_id')
|
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['stake_amount'] = 15.0
|
conf['stake_amount'] = 15.0
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -369,12 +327,12 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
|
|||||||
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
||||||
|
|
||||||
assert int(fields[0]) == 1
|
assert int(fields[0]) == 1
|
||||||
assert fields[1] == 'BTC_ETH'
|
assert fields[1] == 'ETH/BTC'
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order,
|
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _daily() method
|
Test _daily() method
|
||||||
"""
|
"""
|
||||||
@ -385,19 +343,21 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order,
|
|||||||
return_value=15000.0
|
return_value=15000.0
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -453,7 +413,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker
|
||||||
)
|
)
|
||||||
@ -461,11 +421,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
@ -484,8 +444,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
|
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
|
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _profit() method
|
Test _profit() method
|
||||||
"""
|
"""
|
||||||
@ -493,19 +453,21 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
|
|||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._profit(bot=MagicMock(), update=update)
|
telegram._profit(bot=MagicMock(), update=update)
|
||||||
@ -526,7 +488,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
@ -541,49 +503,42 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
|
|||||||
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _balance() method
|
Test _balance() method
|
||||||
"""
|
"""
|
||||||
mock_balance = [
|
|
||||||
{
|
mock_balance = {
|
||||||
'Currency': 'BTC',
|
'BTC': {
|
||||||
'Balance': 10.0,
|
'total': 12.0,
|
||||||
'Available': 12.0,
|
'free': 12.0,
|
||||||
'Pending': 0.0,
|
'used': 0.0
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
},
|
},
|
||||||
{
|
'ETH': {
|
||||||
'Currency': 'ETH',
|
'total': 0.0,
|
||||||
'Balance': 0.0,
|
'free': 0.0,
|
||||||
'Available': 0.0,
|
'used': 0.0
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
},
|
},
|
||||||
{
|
'USDT': {
|
||||||
'Currency': 'USDT',
|
'total': 10000.0,
|
||||||
'Balance': 10000.0,
|
'free': 10000.0,
|
||||||
'Available': 0.0,
|
'used': 0.0
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
},
|
},
|
||||||
{
|
'LTC': {
|
||||||
'Currency': 'LTC',
|
'total': 10.0,
|
||||||
'Balance': 10.0,
|
'free': 10.0,
|
||||||
'Available': 10.0,
|
'used': 0.0
|
||||||
'Pending': 0.0,
|
|
||||||
'CryptoAddress': 'XXXX',
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
|
||||||
def mock_ticker(symbol, refresh):
|
def mock_ticker(symbol, refresh):
|
||||||
"""
|
"""
|
||||||
Mock Bittrex.get_ticker() response
|
Mock Bittrex.get_ticker() response
|
||||||
"""
|
"""
|
||||||
if symbol == 'USDT_BTC':
|
if symbol == 'BTC/USDT':
|
||||||
return {
|
return {
|
||||||
'bid': 10000.00,
|
'bid': 10000.00,
|
||||||
'ask': 10000.00,
|
'ask': 10000.00,
|
||||||
@ -598,29 +553,28 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=mock_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', side_effect=mock_ticker)
|
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
result = msg_mock.call_args_list[0][0][0]
|
result = msg_mock.call_args_list[0][0][0]
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*Currency*: BTC' in result
|
assert '*BTC:*' in result
|
||||||
assert '*Currency*: ETH' not in result
|
assert '*ETH:*' not in result
|
||||||
assert '*Currency*: USDT' in result
|
assert '*USDT:*' in result
|
||||||
assert 'Balance' in result
|
assert 'Balance:' in result
|
||||||
assert 'Est. BTC' in result
|
assert 'Est. BTC:' in result
|
||||||
assert '*BTC*: 12.00000000' in result
|
assert 'BTC: 14.00000000' in result
|
||||||
|
|
||||||
|
|
||||||
def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
||||||
@ -628,18 +582,16 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
Test _balance() method when the Exchange platform returns nothing
|
Test _balance() method when the Exchange platform returns nothing
|
||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=[])
|
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
@ -652,41 +604,35 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
Test _start() method
|
Test _start() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
telegram._start(bot=MagicMock(), update=update)
|
telegram._start(bot=MagicMock(), update=update)
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
assert msg_mock.call_count == 0
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _start() method
|
Test _start() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
@ -702,16 +648,14 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
|||||||
Test _stop() method
|
Test _stop() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
@ -727,16 +671,14 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
Test _stop() method
|
Test _stop() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -747,7 +689,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_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker) -> None:
|
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||||
|
""" Test _reload_conf() method """
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, 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, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _forcesell() method
|
Test _forcesell() method
|
||||||
"""
|
"""
|
||||||
@ -757,12 +721,14 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
|
|||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -772,21 +738,22 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
|
|||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
# Increase the price and sell it
|
# Increase the price and sell it
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
|
||||||
|
|
||||||
update.message.text = '/forcesell 1'
|
update.message.text = '/forcesell 1'
|
||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker) -> None:
|
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
|
ticker_sell_down, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _forcesell() method
|
Test _forcesell() method
|
||||||
"""
|
"""
|
||||||
@ -796,12 +763,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
|
|||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -809,7 +778,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
|
|||||||
|
|
||||||
# Decrease the price and sell it
|
# Decrease the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker_sell_down
|
get_ticker=ticker_sell_down
|
||||||
)
|
)
|
||||||
@ -822,14 +791,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
|
|||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
|
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
|
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _forcesell() method
|
Test _forcesell() method
|
||||||
"""
|
"""
|
||||||
@ -838,13 +807,16 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
|
|||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -873,11 +845,11 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -904,8 +876,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
|
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, update, ticker, limit_buy_order,
|
def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _performance() method
|
Test _performance() method
|
||||||
"""
|
"""
|
||||||
@ -915,15 +887,17 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -942,7 +916,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
|
|||||||
telegram._performance(bot=MagicMock(), update=update)
|
telegram._performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>BTC_ETH\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
@ -955,10 +929,10 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -968,7 +942,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _count() method
|
Test _count() method
|
||||||
"""
|
"""
|
||||||
@ -978,15 +952,17 @@ def test_count_handle(default_conf, update, ticker, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_order_id')
|
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||||
|
get_markets=markets
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -1015,14 +991,14 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
Test _help() method
|
Test _help() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._help(bot=MagicMock(), update=update)
|
telegram._help(bot=MagicMock(), update=update)
|
||||||
@ -1035,14 +1011,13 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
|||||||
Test _version() method
|
Test _version() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._version(bot=MagicMock(), update=update)
|
telegram._version(bot=MagicMock(), update=update)
|
||||||
@ -1055,20 +1030,14 @@ def test_send_msg(default_conf, mocker) -> None:
|
|||||||
Test send_msg() method
|
Test send_msg() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = False
|
|
||||||
telegram.send_msg('test', bot)
|
|
||||||
assert not bot.method_calls
|
|
||||||
bot.reset_mock()
|
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
telegram.send_msg('test', bot)
|
telegram._send_msg('test', bot)
|
||||||
assert len(bot.method_calls) == 1
|
assert len(bot.method_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -1077,16 +1046,15 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
|||||||
Test send_msg() method
|
Test send_msg() method
|
||||||
"""
|
"""
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
telegram.send_msg('test', bot)
|
telegram._send_msg('test', bot)
|
||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
||||||
|
@ -9,7 +9,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ def test_default_strategy(result):
|
|||||||
|
|
||||||
assert type(strategy.minimal_roi) is dict
|
assert type(strategy.minimal_roi) is dict
|
||||||
assert type(strategy.stoploss) is float
|
assert type(strategy.stoploss) is float
|
||||||
assert type(strategy.ticker_interval) is int
|
assert type(strategy.ticker_interval) is str
|
||||||
indicators = strategy.populate_indicators(result)
|
indicators = strategy.populate_indicators(result)
|
||||||
assert type(indicators) is DataFrame
|
assert type(indicators) is DataFrame
|
||||||
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
||||||
|
@ -1,14 +1,39 @@
|
|||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.strategy import import_strategy
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_strategy(caplog):
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
strategy = DefaultStrategy()
|
||||||
|
strategy.some_method = lambda *args, **kwargs: 42
|
||||||
|
|
||||||
|
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
|
||||||
|
assert strategy.some_method() == 42
|
||||||
|
|
||||||
|
imported_strategy = import_strategy(strategy)
|
||||||
|
|
||||||
|
assert dir(strategy) == dir(imported_strategy)
|
||||||
|
|
||||||
|
assert imported_strategy.__module__ == 'freqtrade.strategy'
|
||||||
|
assert imported_strategy.some_method() == 42
|
||||||
|
|
||||||
|
assert (
|
||||||
|
'freqtrade.strategy',
|
||||||
|
logging.DEBUG,
|
||||||
|
'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
|
||||||
|
'as freqtrade.strategy.DefaultStrategy',
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
def test_search_strategy():
|
def test_search_strategy():
|
||||||
default_location = os.path.join(os.path.dirname(
|
default_location = os.path.join(os.path.dirname(
|
||||||
os.path.realpath(__file__)), '..', '..', 'strategy'
|
os.path.realpath(__file__)), '..', '..', 'strategy'
|
||||||
@ -20,19 +45,21 @@ def test_search_strategy():
|
|||||||
|
|
||||||
|
|
||||||
def test_load_strategy(result):
|
def test_load_strategy(result):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
||||||
resolver._load_strategy('TestStrategy')
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_custom_directory(result):
|
def test_load_strategy_invalid_directory(result, caplog):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver()
|
||||||
extra_dir = os.path.join('some', 'path')
|
extra_dir = os.path.join('some', 'path')
|
||||||
with pytest.raises(
|
resolver._load_strategy('TestStrategy', extra_dir)
|
||||||
FileNotFoundError,
|
|
||||||
match=r".*No such file or directory: '{}'".format(extra_dir)):
|
assert (
|
||||||
resolver._load_strategy('TestStrategy', extra_dir)
|
'freqtrade.strategy.resolver',
|
||||||
|
logging.WARNING,
|
||||||
|
'Path "{}" does not exist'.format(extra_dir),
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
import freqtrade.tests.conftest as tt # test tools
|
import freqtrade.tests.conftest as tt # test tools
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
# whitelist, blacklist, filtering, all of that will
|
# whitelist, blacklist, filtering, all of that will
|
||||||
# eventually become some rules to run on a generic ACL engine
|
# eventually become some rules to run on a generic ACL engine
|
||||||
@ -12,118 +13,60 @@ def whitelist_conf():
|
|||||||
|
|
||||||
config['stake_currency'] = 'BTC'
|
config['stake_currency'] = 'BTC'
|
||||||
config['exchange']['pair_whitelist'] = [
|
config['exchange']['pair_whitelist'] = [
|
||||||
'BTC_ETH',
|
'ETH/BTC',
|
||||||
'BTC_TKN',
|
'TKN/BTC',
|
||||||
'BTC_TRST',
|
'TRST/BTC',
|
||||||
'BTC_SWT',
|
'SWT/BTC',
|
||||||
'BTC_BCC'
|
'BCC/BTC'
|
||||||
]
|
]
|
||||||
|
|
||||||
config['exchange']['pair_blacklist'] = [
|
config['exchange']['pair_blacklist'] = [
|
||||||
'BTC_BLK'
|
'BLK/BTC'
|
||||||
]
|
]
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_market_summaries():
|
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
|
||||||
return [{
|
|
||||||
'MarketName': 'BTC-TKN',
|
|
||||||
'High': 0.00000919,
|
|
||||||
'Low': 0.00000820,
|
|
||||||
'Volume': 74339.61396015,
|
|
||||||
'Last': 0.00000820,
|
|
||||||
'BaseVolume': 1664,
|
|
||||||
'TimeStamp': '2014-07-09T07:19:30.15',
|
|
||||||
'Bid': 0.00000820,
|
|
||||||
'Ask': 0.00000831,
|
|
||||||
'OpenBuyOrders': 15,
|
|
||||||
'OpenSellOrders': 15,
|
|
||||||
'PrevDay': 0.00000821,
|
|
||||||
'Created': '2014-03-20T06:00:00',
|
|
||||||
'DisplayMarketName': ''
|
|
||||||
}, {
|
|
||||||
'MarketName': 'BTC-ETH',
|
|
||||||
'High': 0.00000072,
|
|
||||||
'Low': 0.00000001,
|
|
||||||
'Volume': 166340678.42280999,
|
|
||||||
'Last': 0.00000005,
|
|
||||||
'BaseVolume': 42,
|
|
||||||
'TimeStamp': '2014-07-09T07:21:40.51',
|
|
||||||
'Bid': 0.00000004,
|
|
||||||
'Ask': 0.00000005,
|
|
||||||
'OpenBuyOrders': 18,
|
|
||||||
'OpenSellOrders': 18,
|
|
||||||
'PrevDay': 0.00000002,
|
|
||||||
'Created': '2014-05-30T07:57:49.637',
|
|
||||||
'DisplayMarketName': ''
|
|
||||||
}, {
|
|
||||||
'MarketName': 'BTC-BLK',
|
|
||||||
'High': 0.00000072,
|
|
||||||
'Low': 0.00000001,
|
|
||||||
'Volume': 166340678.42280999,
|
|
||||||
'Last': 0.00000005,
|
|
||||||
'BaseVolume': 3,
|
|
||||||
'TimeStamp': '2014-07-09T07:21:40.51',
|
|
||||||
'Bid': 0.00000004,
|
|
||||||
'Ask': 0.00000005,
|
|
||||||
'OpenBuyOrders': 18,
|
|
||||||
'OpenSellOrders': 18,
|
|
||||||
'PrevDay': 0.00000002,
|
|
||||||
'Created': '2014-05-30T07:57:49.637',
|
|
||||||
'DisplayMarketName': ''
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
|
||||||
def get_health():
|
|
||||||
return [{'Currency': 'ETH', 'IsActive': True},
|
|
||||||
{'Currency': 'TKN', 'IsActive': True},
|
|
||||||
{'Currency': 'BLK', 'IsActive': True}]
|
|
||||||
|
|
||||||
|
|
||||||
def get_health_empty():
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_market_pair_not_in_whitelist(mocker):
|
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
|
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
|
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
||||||
)
|
)
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist(mocker):
|
def test_refresh_whitelist(mocker, markets):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic(mocker):
|
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_wallet_health=get_health,
|
get_markets=markets,
|
||||||
get_market_summaries=get_market_summaries
|
get_tickers=tickers,
|
||||||
|
exchange_has=MagicMock(return_value=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = ['BTC_TKN', 'BTC_ETH']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
||||||
@ -132,10 +75,10 @@ def test_refresh_whitelist_dynamic(mocker):
|
|||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic_empty(mocker):
|
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = []
|
whitelist = []
|
||||||
|
@ -13,7 +13,8 @@ from pandas import DataFrame
|
|||||||
|
|
||||||
from freqtrade.analyze import Analyze, SignalType
|
from freqtrade.analyze import Analyze, SignalType
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
# Avoid to reinit the same object again and again
|
||||||
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
|
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
|
||||||
@ -45,12 +46,12 @@ 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):
|
||||||
assert result.columns.tolist() == \
|
assert result.columns.tolist() == \
|
||||||
['date', 'close', 'high', 'low', 'open', 'volume']
|
['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
@ -65,16 +66,16 @@ def test_populates_sell_trend(result):
|
|||||||
assert 'sell' in dataframe.columns
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker, default_conf):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
analyze_ticker=MagicMock(
|
analyze_ticker=MagicMock(
|
||||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
@ -82,11 +83,12 @@ def test_returns_latest_buy_signal(mocker):
|
|||||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker):
|
def test_returns_latest_sell_signal(mocker, default_conf):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
analyze_ticker=MagicMock(
|
analyze_ticker=MagicMock(
|
||||||
@ -94,7 +96,7 @@ def test_returns_latest_sell_signal(mocker):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
@ -102,45 +104,49 @@ def test_returns_latest_sell_signal(mocker):
|
|||||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None)
|
||||||
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
||||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
analyze_ticker=MagicMock(
|
analyze_ticker=MagicMock(
|
||||||
side_effect=ValueError('xyz')
|
side_effect=ValueError('xyz')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
||||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
analyze_ticker=MagicMock(
|
analyze_ticker=MagicMock(
|
||||||
return_value=DataFrame([])
|
return_value=DataFrame([])
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
|
||||||
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
||||||
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
||||||
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||||
@ -150,15 +156,16 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
|||||||
return_value=DataFrame(ticks)
|
return_value=DataFrame(ticks)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker):
|
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.Analyze',
|
'freqtrade.analyze.Analyze',
|
||||||
analyze_ticker=MagicMock(
|
analyze_ticker=MagicMock(
|
||||||
@ -166,20 +173,16 @@ def test_get_signal_handles_exceptions(mocker):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
|
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
|
def test_parse_ticker_dataframe(ticker_history):
|
||||||
columns = ['date', 'close', 'high', 'low', 'open', 'volume']
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
# Test file with BV data
|
# Test file with BV data
|
||||||
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
|
|
||||||
# Test file without BV data
|
|
||||||
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
|
|
||||||
assert dataframe.columns.tolist() == columns
|
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
@ -187,8 +190,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
analyze = Analyze(default_conf)
|
analyze = Analyze(default_conf)
|
||||||
|
|
||||||
timerange = ((None, 'line'), None, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['BTC_UNITEST']) == 100
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
|
||||||
|
|
||||||
def test_arguments_object() -> None:
|
def test_arguments_object() -> None:
|
||||||
@ -46,6 +46,11 @@ def test_parse_args_config() -> None:
|
|||||||
assert args.config == '/dev/null'
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_db_url() -> None:
|
||||||
|
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
|
||||||
|
assert args.db_url == 'sqlite:///test.sqlite'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_verbose() -> None:
|
def test_parse_args_verbose() -> None:
|
||||||
args = Arguments(['-v'], '').get_parsed_arg()
|
args = Arguments(['-v'], '').get_parsed_arg()
|
||||||
assert args.loglevel == logging.DEBUG
|
assert args.loglevel == logging.DEBUG
|
||||||
@ -55,10 +60,10 @@ def test_parse_args_verbose() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_scripts_options() -> None:
|
def test_scripts_options() -> None:
|
||||||
arguments = Arguments(['-p', 'BTC_ETH'], '')
|
arguments = Arguments(['-p', 'ETH/BTC'], '')
|
||||||
arguments.scripts_options()
|
arguments.scripts_options()
|
||||||
args = arguments.get_parsed_arg()
|
args = arguments.get_parsed_arg()
|
||||||
assert args.pair == 'BTC_ETH'
|
assert args.pair == 'ETH/BTC'
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_version() -> None:
|
def test_parse_args_version() -> None:
|
||||||
@ -107,8 +112,25 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_timerange_incorrect() -> None:
|
def test_parse_timerange_incorrect() -> None:
|
||||||
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
|
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
|
||||||
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
|
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
|
||||||
|
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
|
||||||
|
|
||||||
|
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
|
||||||
|
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
|
||||||
|
timerange = Arguments.parse_timerange('20100522-20150730')
|
||||||
|
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
|
||||||
|
|
||||||
|
# Added test for unix timestamp - BTC genesis date
|
||||||
|
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
|
||||||
|
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
|
||||||
|
timerange = Arguments.parse_timerange('1231006505-1233360000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
|
||||||
|
|
||||||
|
# TODO: Find solution for the following case (passing timestamp in ms)
|
||||||
|
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
|
||||||
|
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
|
||||||
|
|
||||||
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||||
Arguments.parse_timerange('-')
|
Arguments.parse_timerange('-')
|
||||||
|
|
||||||
@ -126,7 +148,7 @@ def test_parse_args_backtesting_custom() -> None:
|
|||||||
'-c', 'test_conf.json',
|
'-c', 'test_conf.json',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--live',
|
'--live',
|
||||||
'--ticker-interval', '1',
|
'--ticker-interval', '1m',
|
||||||
'--refresh-pairs-cached']
|
'--refresh-pairs-cached']
|
||||||
call_args = Arguments(args, '').get_parsed_arg()
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
assert call_args.config == 'test_conf.json'
|
assert call_args.config == 'test_conf.json'
|
||||||
@ -134,7 +156,7 @@ def test_parse_args_backtesting_custom() -> None:
|
|||||||
assert call_args.loglevel == logging.INFO
|
assert call_args.loglevel == logging.INFO
|
||||||
assert call_args.subparser == 'backtesting'
|
assert call_args.subparser == 'backtesting'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
assert call_args.ticker_interval == 1
|
assert call_args.ticker_interval == '1m'
|
||||||
assert call_args.refresh_pairs is True
|
assert call_args.refresh_pairs is True
|
||||||
|
|
||||||
|
|
||||||
@ -152,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'
|
||||||
|
@ -6,13 +6,16 @@ Unit test file for configuration.py
|
|||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import ValidationError
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
|
|
||||||
def test_configuration_object() -> None:
|
def test_configuration_object() -> None:
|
||||||
@ -28,19 +31,19 @@ def test_configuration_object() -> None:
|
|||||||
assert hasattr(Configuration, 'get_config')
|
assert hasattr(Configuration, 'get_config')
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_invalid_pair(default_conf, mocker) -> None:
|
def test_load_config_invalid_pair(default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test the configuration validator with an invalid PAIR format
|
Test the configuration validator with an invalid PAIR format
|
||||||
"""
|
"""
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_missing_attributes(default_conf, mocker) -> None:
|
def test_load_config_missing_attributes(default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test the configuration validator with a missing attribute
|
Test the configuration validator with a missing attribute
|
||||||
"""
|
"""
|
||||||
@ -48,7 +51,19 @@ def test_load_config_missing_attributes(default_conf, mocker) -> None:
|
|||||||
conf.pop('exchange')
|
conf.pop('exchange')
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with a missing attribute
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['stake_amount'] = 'fake'
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +75,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
configuration = Configuration([])
|
configuration = Configuration(Namespace())
|
||||||
validated_conf = configuration._load_config_file('somefile')
|
validated_conf = configuration._load_config_file('somefile')
|
||||||
assert file_mock.call_count == 1
|
assert file_mock.call_count == 1
|
||||||
assert validated_conf.items() >= default_conf.items()
|
assert validated_conf.items() >= default_conf.items()
|
||||||
@ -68,7 +83,22 @@ def test_load_config_file(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_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration._load_config_file() method
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['max_open_trades'] = 0
|
||||||
|
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
Configuration(Namespace())._load_config_file('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Configuration._load_config_file() method
|
Test Configuration._load_config_file() method
|
||||||
"""
|
"""
|
||||||
@ -76,14 +106,10 @@ def test_load_config_file_exception(mocker, caplog) -> None:
|
|||||||
'freqtrade.configuration.open',
|
'freqtrade.configuration.open',
|
||||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
)
|
)
|
||||||
configuration = Configuration([])
|
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:
|
||||||
@ -101,7 +127,6 @@ def test_load_config(default_conf, mocker) -> None:
|
|||||||
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||||
assert validated_conf.get('strategy_path') is None
|
assert validated_conf.get('strategy_path') is None
|
||||||
assert 'dynamic_whitelist' not in validated_conf
|
assert 'dynamic_whitelist' not in validated_conf
|
||||||
assert 'dry_run_db' not in validated_conf
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
@ -112,13 +137,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--strategy-path', '/some/path',
|
'--strategy-path', '/some/path',
|
||||||
'--dry-run-db',
|
'--db-url', 'sqlite:///someurl',
|
||||||
]
|
]
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
@ -126,7 +151,44 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
assert validated_conf.get('dynamic_whitelist') == 10
|
assert validated_conf.get('dynamic_whitelist') == 10
|
||||||
assert validated_conf.get('strategy') == 'TestStrategy'
|
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||||
assert validated_conf.get('strategy_path') == '/some/path'
|
assert validated_conf.get('strategy_path') == '/some/path'
|
||||||
assert validated_conf.get('dry_run_db') is True
|
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||||
|
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = False
|
||||||
|
del conf["db_url"]
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
||||||
|
|
||||||
|
# Test dry=run with ProdURL
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = True
|
||||||
|
conf["db_url"] = DEFAULT_DB_PROD_URL
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
|
||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
@ -158,12 +220,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--dry-run-db'
|
'--db-url', 'sqlite:///tmp/testdb',
|
||||||
]
|
]
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
configuration.get_config()
|
configuration.get_config()
|
||||||
@ -175,23 +237,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
assert log_has(
|
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||||
'Parameter --dry-run-db detected ...',
|
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
assert log_has(
|
|
||||||
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the Dry run condition
|
|
||||||
configuration.config.update({'dry_run': False})
|
|
||||||
configuration._load_common_config(configuration.config)
|
|
||||||
assert log_has(
|
|
||||||
'Dry run is disabled. (--dry_run_db ignored)',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
@ -202,13 +249,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'backtesting'
|
'backtesting'
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -219,7 +266,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
@ -246,12 +293,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
'--datadir', '/foo/bar',
|
'--datadir', '/foo/bar',
|
||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--realistic-simulation',
|
'--realistic-simulation',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
@ -259,7 +306,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo'
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -270,13 +317,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'pair_whitelist' in config['exchange']
|
assert 'pair_whitelist' in config['exchange']
|
||||||
assert 'datadir' in config
|
assert 'datadir' in config
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert 'ticker_interval' in config
|
assert 'ticker_interval' in config
|
||||||
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Using ticker_interval: 1 ...',
|
'Using ticker_interval: 1m ...',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -310,14 +357,13 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
arglist = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--epochs', '10',
|
'--epochs', '10',
|
||||||
'--use-mongodb',
|
|
||||||
'--spaces', 'all',
|
'--spaces', 'all',
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(args, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
config = configuration.get_config()
|
config = configuration.get_config()
|
||||||
@ -327,10 +373,32 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
|
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
|
||||||
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'mongodb' in config
|
|
||||||
assert config['mongodb'] is True
|
|
||||||
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert config['spaces'] == ['all']
|
assert config['spaces'] == ['all']
|
||||||
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_exchange(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with a missing attribute
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
|
# Test a valid exchange
|
||||||
|
conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
|
assert configuration.check_exchange(conf)
|
||||||
|
|
||||||
|
# Test a valid exchange
|
||||||
|
conf.get('exchange').update({'name': 'binance'})
|
||||||
|
assert configuration.check_exchange(conf)
|
||||||
|
|
||||||
|
# Test a invalid exchange
|
||||||
|
conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||||
|
configuration.config = conf
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match=r'.*Exchange "unknown_exchange" not supported.*'
|
||||||
|
):
|
||||||
|
configuration.check_exchange(conf)
|
||||||
|
@ -6,11 +6,11 @@ from freqtrade.analyze import Analyze
|
|||||||
from freqtrade.optimize import load_data
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
_pairs = ['BTC_ETH']
|
_pairs = ['ETH/BTC']
|
||||||
|
|
||||||
|
|
||||||
def load_dataframe_pair(pairs):
|
def load_dataframe_pair(pairs):
|
||||||
ld = load_data(None, ticker_interval=5, pairs=pairs)
|
ld = load_data(None, ticker_interval='5m', pairs=pairs)
|
||||||
assert isinstance(ld, dict)
|
assert isinstance(ld, dict)
|
||||||
assert isinstance(pairs[0], str)
|
assert isinstance(pairs[0], str)
|
||||||
dataframe = ld[pairs[0]]
|
dataframe = ld[pairs[0]]
|
||||||
|
@ -6,7 +6,10 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||||
|
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
def test_pair_convertion_object():
|
def test_pair_convertion_object():
|
||||||
@ -37,7 +40,8 @@ def test_pair_convertion_object():
|
|||||||
assert pair_convertion.price == 30000.123
|
assert pair_convertion.price == 30000.123
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_is_supported():
|
def test_fiat_convert_is_supported(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
||||||
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
||||||
@ -45,7 +49,9 @@ def test_fiat_convert_is_supported():
|
|||||||
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_add_pair():
|
def test_fiat_convert_add_pair(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
pair_len = len(fiat_convert._pairs)
|
pair_len = len(fiat_convert._pairs)
|
||||||
@ -67,18 +73,14 @@ def test_fiat_convert_add_pair():
|
|||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_find_price(mocker):
|
def test_fiat_convert_find_price(mocker):
|
||||||
api_mock = MagicMock(return_value={
|
patch_coinmarketcap(mocker)
|
||||||
'price_usd': 12345.0,
|
|
||||||
'price_eur': 13000.2
|
|
||||||
})
|
|
||||||
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
||||||
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'The crypto symbol XRP is not supported.'):
|
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
|
||||||
fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD')
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
|
||||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
||||||
@ -88,12 +90,17 @@ 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=[])
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
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={
|
patch_coinmarketcap(mocker)
|
||||||
'price_usd': 28000.0,
|
|
||||||
'price_eur': 15000.0
|
|
||||||
})
|
|
||||||
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
@ -124,12 +131,74 @@ def test_fiat_convert_get_price(mocker):
|
|||||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_without_network():
|
def test_fiat_convert_same_currencies(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_two_FIAT(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_loadcryptomap(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
assert len(fiat_convert._cryptomap) == 2
|
||||||
|
|
||||||
|
assert fiat_convert._cryptomap["BTC"] == "1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_init_network_exception(mocker):
|
||||||
|
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||||
|
listmock = MagicMock(side_effect=RequestException)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
listings=listmock,
|
||||||
|
)
|
||||||
|
# with pytest.raises(RequestEsxception):
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
fiat_convert._cryptomap = {}
|
||||||
|
fiat_convert._load_cryptomap()
|
||||||
|
|
||||||
|
length_cryptomap = len(fiat_convert._cryptomap)
|
||||||
|
assert length_cryptomap == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_without_network(mocker):
|
||||||
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
cmc_temp = CryptoToFiatConverter._coinmarketcap
|
||||||
CryptoToFiatConverter._coinmarketcap = None
|
CryptoToFiatConverter._coinmarketcap = None
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_amount(mocker):
|
||||||
|
patch_coinmarketcap(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
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,12 +3,17 @@ 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.tests.conftest import log_has
|
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, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting(mocker) -> None:
|
def test_parse_args_backtesting(mocker) -> None:
|
||||||
@ -60,34 +65,153 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
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.
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
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 """
|
||||||
|
patch_exchange(mocker)
|
||||||
|
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']
|
||||||
|
@ -9,7 +9,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
||||||
common_datearray, file_dump_json)
|
common_datearray, file_dump_json, format_ms_time)
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
|
||||||
|
|
||||||
@ -39,24 +39,24 @@ 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, mocker) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
"""
|
"""
|
||||||
Test common_datearray()
|
Test common_datearray()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
analyze = Analyze(default_conf)
|
analyze = Analyze(default_conf)
|
||||||
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'BTC_UNITEST': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
dates = common_datearray(dataframes)
|
dates = common_datearray(dataframes)
|
||||||
|
|
||||||
assert dates.size == dataframes['BTC_UNITEST']['date'].size
|
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
|
||||||
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
|
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
|
||||||
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
|
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1]
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json(mocker) -> None:
|
def test_file_dump_json(mocker) -> None:
|
||||||
@ -69,3 +69,25 @@ def test_file_dump_json(mocker) -> None:
|
|||||||
file_dump_json('somefile', [1, 2, 3])
|
file_dump_json('somefile', [1, 2, 3])
|
||||||
assert file_open.call_count == 1
|
assert file_open.call_count == 1
|
||||||
assert json_dump.call_count == 1
|
assert json_dump.call_count == 1
|
||||||
|
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
|
||||||
|
json_dump = mocker.patch('json.dump', MagicMock())
|
||||||
|
file_dump_json('somefile', [1, 2, 3], True)
|
||||||
|
assert file_open.call_count == 1
|
||||||
|
assert json_dump.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_ms_time() -> None:
|
||||||
|
"""
|
||||||
|
test format_ms_time()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Date 2018-04-10 18:02:01
|
||||||
|
date_in_epoch_ms = 1523383321000
|
||||||
|
date = format_ms_time(date_in_epoch_ms)
|
||||||
|
assert type(date) is str
|
||||||
|
res = datetime.datetime(2018, 4, 10, 18, 2, 1, tzinfo=datetime.timezone.utc)
|
||||||
|
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
||||||
|
res = datetime.datetime(2017, 12, 13, 8, 2, 1, tzinfo=datetime.timezone.utc)
|
||||||
|
# Date 2017-12-13 08:02:01
|
||||||
|
date_in_epoch_ms = 1513152121000
|
||||||
|
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import os
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade import constants, OperationalException
|
||||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||||
|
|
||||||
|
|
||||||
@ -13,90 +14,60 @@ def init_persistence(default_conf):
|
|||||||
init(default_conf)
|
init(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_init_create_session(default_conf, mocker):
|
def test_init_create_session(default_conf):
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
|
||||||
|
|
||||||
# Check if init create a session
|
# Check if init create a session
|
||||||
init(default_conf)
|
init(default_conf)
|
||||||
assert hasattr(Trade, 'session')
|
assert hasattr(Trade, 'session')
|
||||||
assert 'Session' in type(Trade.session).__name__
|
assert 'Session' in type(Trade.session).__name__
|
||||||
|
|
||||||
|
|
||||||
def test_init_dry_run_db(default_conf, mocker):
|
def test_init_custom_db_url(default_conf, mocker):
|
||||||
default_conf.update({'dry_run_db': True})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
# Update path to a value other than default, but still in-memory
|
||||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||||
dry_run_db_swp = dry_run_db + '.swp'
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
if os.path.isfile(dry_run_db):
|
init(conf)
|
||||||
os.rename(dry_run_db, dry_run_db_swp)
|
assert create_engine_mock.call_count == 1
|
||||||
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||||
# Check if the new tradesv3.dry_run.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(dry_run_db) is True
|
|
||||||
|
|
||||||
# Delete the file made for this unitest and rollback to the previous
|
|
||||||
# tradesv3.dry_run.sqlite file
|
|
||||||
|
|
||||||
# 1. Delete file from the test
|
|
||||||
if os.path.isfile(dry_run_db):
|
|
||||||
os.remove(dry_run_db)
|
|
||||||
|
|
||||||
# 2. Rollback to the initial file
|
|
||||||
if os.path.isfile(dry_run_db_swp):
|
|
||||||
os.rename(dry_run_db_swp, dry_run_db)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_dry_run_without_db(default_conf, mocker):
|
def test_init_invalid_db_url(default_conf):
|
||||||
default_conf.update({'dry_run_db': False})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
# Update path to a value other than default, but still in-memory
|
||||||
dry_run_db = 'tradesv3.dry_run.sqlite'
|
conf.update({'db_url': 'unknown:///some.url'})
|
||||||
dry_run_db_swp = dry_run_db + '.swp'
|
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||||
|
init(conf)
|
||||||
if os.path.isfile(dry_run_db):
|
|
||||||
os.rename(dry_run_db, dry_run_db_swp)
|
|
||||||
|
|
||||||
# Check if the new tradesv3.dry_run.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(dry_run_db) is False
|
|
||||||
|
|
||||||
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
|
|
||||||
if os.path.isfile(dry_run_db_swp):
|
|
||||||
os.rename(dry_run_db_swp, dry_run_db)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_prod_db(default_conf, mocker):
|
def test_init_prod_db(default_conf, mocker):
|
||||||
default_conf.update({'dry_run': False})
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
conf.update({'dry_run': False})
|
||||||
|
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||||
|
|
||||||
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
prod_db = 'tradesv3.sqlite'
|
|
||||||
prod_db_swp = prod_db + '.swp'
|
|
||||||
|
|
||||||
if os.path.isfile(prod_db):
|
init(conf)
|
||||||
os.rename(prod_db, prod_db_swp)
|
assert create_engine_mock.call_count == 1
|
||||||
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
# Check if the new tradesv3.sqlite was created
|
|
||||||
init(default_conf)
|
|
||||||
assert os.path.isfile(prod_db) is True
|
|
||||||
|
|
||||||
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
|
def test_init_dryrun_db(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'dry_run': True})
|
||||||
|
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||||
|
|
||||||
# 1. Delete file from the test
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
if os.path.isfile(prod_db):
|
|
||||||
os.remove(prod_db)
|
|
||||||
|
|
||||||
# Rollback to the initial 'tradesv3.sqlite' file
|
init(conf)
|
||||||
if os.path.isfile(prod_db_swp):
|
assert create_engine_mock.call_count == 1
|
||||||
os.rename(prod_db_swp, prod_db)
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
|
||||||
"""
|
"""
|
||||||
On this test we will buy and sell a crypto currency.
|
On this test we will buy and sell a crypto currency.
|
||||||
|
|
||||||
@ -125,10 +96,11 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.open_rate is None
|
assert trade.open_rate is None
|
||||||
@ -151,12 +123,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
|
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
@ -174,12 +147,13 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_close_trade_price_exception(limit_buy_order):
|
def test_calc_close_trade_price_exception(limit_buy_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
|
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
@ -190,10 +164,11 @@ def test_calc_close_trade_price_exception(limit_buy_order):
|
|||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_open_order(limit_buy_order):
|
def test_update_open_order(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=1.00,
|
stake_amount=1.00,
|
||||||
fee=0.1,
|
fee_open=0.1,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=0.1,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
@ -201,7 +176,7 @@ def test_update_open_order(limit_buy_order):
|
|||||||
assert trade.close_profit is None
|
assert trade.close_profit is None
|
||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
|
|
||||||
limit_buy_order['closed'] = False
|
limit_buy_order['status'] = 'open'
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
@ -213,10 +188,11 @@ def test_update_open_order(limit_buy_order):
|
|||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_invalid_order(limit_buy_order):
|
def test_update_invalid_order(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=1.00,
|
stake_amount=1.00,
|
||||||
fee=0.1,
|
fee_open=0.1,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=0.1,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
limit_buy_order['type'] = 'invalid'
|
limit_buy_order['type'] = 'invalid'
|
||||||
with pytest.raises(ValueError, match=r'Unknown order type'):
|
with pytest.raises(ValueError, match=r'Unknown order type'):
|
||||||
@ -224,12 +200,13 @@ def test_update_invalid_order(limit_buy_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_open_trade_price(limit_buy_order):
|
def test_calc_open_trade_price(limit_buy_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'open_trade'
|
trade.open_order_id = 'open_trade'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
@ -242,12 +219,13 @@ def test_calc_open_trade_price(limit_buy_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'close_trade'
|
trade.open_order_id = 'close_trade'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
@ -264,12 +242,13 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_profit(limit_buy_order, limit_sell_order):
|
def test_calc_profit(limit_buy_order, limit_sell_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'profit_percent'
|
trade.open_order_id = 'profit_percent'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
@ -295,12 +274,13 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
exchange=Exchanges.BITTREX,
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
)
|
)
|
||||||
trade.open_order_id = 'profit_percent'
|
trade.open_order_id = 'profit_percent'
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
@ -319,40 +299,43 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
|||||||
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
||||||
|
|
||||||
|
|
||||||
def test_clean_dry_run_db(default_conf):
|
def test_clean_dry_run_db(default_conf, fee):
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=123.0,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='BITTREX',
|
exchange='bittrex',
|
||||||
open_order_id='dry_run_buy_12345'
|
open_order_id='dry_run_buy_12345'
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETC',
|
pair='ETC/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=123.0,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='BITTREX',
|
exchange='bittrex',
|
||||||
open_order_id='dry_run_sell_12345'
|
open_order_id='dry_run_sell_12345'
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
|
|
||||||
# Simulate prod entry
|
# Simulate prod entry
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETC',
|
pair='ETC/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
amount=123.0,
|
amount=123.0,
|
||||||
fee=0.0025,
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='BITTREX',
|
exchange='bittrex',
|
||||||
open_order_id='prod_buy_12345'
|
open_order_id='prod_buy_12345'
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
@ -364,3 +347,109 @@ def test_clean_dry_run_db(default_conf):
|
|||||||
|
|
||||||
# We have now only the prod
|
# We have now only the prod
|
||||||
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_old(mocker, default_conf, fee):
|
||||||
|
"""
|
||||||
|
Test Database migration(starting with old pairformat)
|
||||||
|
"""
|
||||||
|
amount = 103.223
|
||||||
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
exchange VARCHAR NOT NULL,
|
||||||
|
pair VARCHAR NOT NULL,
|
||||||
|
is_open BOOLEAN NOT NULL,
|
||||||
|
fee FLOAT NOT NULL,
|
||||||
|
open_rate FLOAT,
|
||||||
|
close_rate FLOAT,
|
||||||
|
close_profit FLOAT,
|
||||||
|
stake_amount FLOAT NOT NULL,
|
||||||
|
amount FLOAT,
|
||||||
|
open_date DATETIME NOT NULL,
|
||||||
|
close_date DATETIME,
|
||||||
|
open_order_id VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_open IN (0, 1))
|
||||||
|
);"""
|
||||||
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||||
|
open_rate, stake_amount, amount, open_date)
|
||||||
|
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
|
||||||
|
0.00258580, {stake}, {amount},
|
||||||
|
'2017-11-28 12:44:24.000000')
|
||||||
|
""".format(fee=fee.return_value,
|
||||||
|
stake=default_conf.get("stake_amount"),
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
|
# Create table using the old format
|
||||||
|
engine.execute(create_table_old)
|
||||||
|
engine.execute(insert_table_old)
|
||||||
|
# Run init to test migration
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
assert trade.fee_open == fee.return_value
|
||||||
|
assert trade.fee_close == fee.return_value
|
||||||
|
assert trade.open_rate_requested is None
|
||||||
|
assert trade.close_rate_requested is None
|
||||||
|
assert trade.is_open == 1
|
||||||
|
assert trade.amount == amount
|
||||||
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
|
assert trade.pair == "ETC/BTC"
|
||||||
|
assert trade.exchange == "bittrex"
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_new(mocker, default_conf, fee):
|
||||||
|
"""
|
||||||
|
Test Database migration (starting with new pairformat)
|
||||||
|
"""
|
||||||
|
amount = 103.223
|
||||||
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
exchange VARCHAR NOT NULL,
|
||||||
|
pair VARCHAR NOT NULL,
|
||||||
|
is_open BOOLEAN NOT NULL,
|
||||||
|
fee FLOAT NOT NULL,
|
||||||
|
open_rate FLOAT,
|
||||||
|
close_rate FLOAT,
|
||||||
|
close_profit FLOAT,
|
||||||
|
stake_amount FLOAT NOT NULL,
|
||||||
|
amount FLOAT,
|
||||||
|
open_date DATETIME NOT NULL,
|
||||||
|
close_date DATETIME,
|
||||||
|
open_order_id VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_open IN (0, 1))
|
||||||
|
);"""
|
||||||
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||||
|
open_rate, stake_amount, amount, open_date)
|
||||||
|
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||||
|
0.00258580, {stake}, {amount},
|
||||||
|
'2019-11-28 12:44:24.000000')
|
||||||
|
""".format(fee=fee.return_value,
|
||||||
|
stake=default_conf.get("stake_amount"),
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
|
# Create table using the old format
|
||||||
|
engine.execute(create_table_old)
|
||||||
|
engine.execute(insert_table_old)
|
||||||
|
# Run init to test migration
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
assert trade.fee_open == fee.return_value
|
||||||
|
assert trade.fee_close == fee.return_value
|
||||||
|
assert trade.open_rate_requested is None
|
||||||
|
assert trade.close_rate_requested is None
|
||||||
|
assert trade.is_open == 1
|
||||||
|
assert trade.amount == amount
|
||||||
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
|
assert trade.pair == "ETC/BTC"
|
||||||
|
assert trade.exchange == "binance"
|
||||||
|
1
freqtrade/tests/testdata/ADA_BTC-1m.json
vendored
Normal file
1
freqtrade/tests/testdata/ADA_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/ADA_BTC-5m.json
vendored
Normal file
1
freqtrade/tests/testdata/ADA_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
File diff suppressed because one or more lines are too long
3
freqtrade/tests/testdata/BTC_UNITEST-8.json
vendored
3
freqtrade/tests/testdata/BTC_UNITEST-8.json
vendored
@ -1,3 +0,0 @@
|
|||||||
[
|
|
||||||
{"O": 0.00162008, "H": 0.00162008, "L": 0.00162008, "C": 0.00162008, "V": 108.14853839, "T": "2017-11-04T23:02:00", "BV": 0.17520927}
|
|
||||||
]
|
|
BIN
freqtrade/tests/testdata/BTC_UNITEST-8.json.gz
vendored
BIN
freqtrade/tests/testdata/BTC_UNITEST-8.json.gz
vendored
Binary file not shown.
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/DASH_BTC-1m.json
vendored
Normal file
1
freqtrade/tests/testdata/DASH_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/DASH_BTC-5m.json
vendored
Normal file
1
freqtrade/tests/testdata/DASH_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/ETC_BTC-1m.json
vendored
Normal file
1
freqtrade/tests/testdata/ETC_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/ETC_BTC-5m.json
vendored
Normal file
1
freqtrade/tests/testdata/ETC_BTC-5m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/ETH_BTC-1m.json
vendored
Normal file
1
freqtrade/tests/testdata/ETH_BTC-1m.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user