mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-14 04:03:55 +00:00
Merge pull request #1438 from freqtrade/release_1804
Release last version for 2018
This commit is contained in:
commit
24e1de91eb
30
.pyup.yml
30
.pyup.yml
|
@ -1,4 +1,32 @@
|
||||||
# autogenerated pyup.io config file
|
# autogenerated pyup.io config file
|
||||||
# see https://pyup.io/docs/configuration/ for all available options
|
# see https://pyup.io/docs/configuration/ for all available options
|
||||||
|
|
||||||
schedule: every day
|
# configure updates globally
|
||||||
|
# default: all
|
||||||
|
# allowed: all, insecure, False
|
||||||
|
update: all
|
||||||
|
|
||||||
|
# configure dependency pinning globally
|
||||||
|
# default: True
|
||||||
|
# allowed: True, False
|
||||||
|
pin: True
|
||||||
|
|
||||||
|
schedule: "every day"
|
||||||
|
|
||||||
|
|
||||||
|
search: False
|
||||||
|
# Specify requirement files by hand, default is empty
|
||||||
|
# default: empty
|
||||||
|
# allowed: list
|
||||||
|
requirements:
|
||||||
|
- requirements.txt
|
||||||
|
- requirements-dev.txt
|
||||||
|
|
||||||
|
|
||||||
|
# configure the branch prefix the bot is using
|
||||||
|
# default: pyup-
|
||||||
|
branch_prefix: pyup/
|
||||||
|
|
||||||
|
# allow to close stale PRs
|
||||||
|
# default: True
|
||||||
|
close_prs: True
|
||||||
|
|
26
.travis.yml
26
.travis.yml
|
@ -1,9 +1,15 @@
|
||||||
sudo: true
|
sudo: true
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
|
dist: trusty
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 3.6
|
- 3.6
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- IMAGE_NAME=freqtradeorg/freqtrade
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
|
@ -11,24 +17,38 @@ addons:
|
||||||
- libdw-dev
|
- libdw-dev
|
||||||
- binutils-dev
|
- binutils-dev
|
||||||
install:
|
install:
|
||||||
- ./install_ta-lib.sh
|
- ./build_helpers/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 pytest-asyncio mypy
|
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- script:
|
- stage: tests
|
||||||
|
script:
|
||||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
- coveralls
|
- coveralls
|
||||||
|
name: pytest
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
|
||||||
|
name: backtest
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
|
||||||
|
name: hyperopt
|
||||||
- script: flake8 freqtrade
|
- script: flake8 freqtrade
|
||||||
|
name: flake8
|
||||||
- script: mypy freqtrade
|
- script: mypy freqtrade
|
||||||
|
name: mypy
|
||||||
|
|
||||||
|
- stage: docker
|
||||||
|
if: branch in (master, develop, feat/improve_travis) AND (type in (push, cron))
|
||||||
|
script:
|
||||||
|
- build_helpers/publish_docker.sh
|
||||||
|
name: "Build and test and push docker image"
|
||||||
|
|
||||||
|
|
||||||
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=
|
||||||
|
|
101
CONTRIBUTING.md
101
CONTRIBUTING.md
|
@ -1,43 +1,54 @@
|
||||||
# Contribute to freqtrade
|
# Contributing
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
## Contribute to freqtrade
|
||||||
|
|
||||||
|
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||||
|
|
||||||
|
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||||
|
|
||||||
|
Few pointers for contributions:
|
||||||
|
|
||||||
- Create your PR against the `develop` branch, not `master`.
|
- Create your PR against the `develop` branch, not `master`.
|
||||||
- New features need to contain unit tests and must be PEP8
|
- New features need to contain unit tests and must be PEP8 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/freqtrade/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:**
|
### 1. Run unit tests
|
||||||
|
|
||||||
## 1. Run unit tests
|
|
||||||
|
|
||||||
All unit tests must pass. If a unit test is broken, change your code to
|
All unit tests must pass. If a unit test is broken, change your code to
|
||||||
make it pass. It means you have introduced a regression.
|
make it pass. It means you have introduced a regression.
|
||||||
|
|
||||||
**Test the whole project**
|
#### Test the whole project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest freqtrade
|
pytest freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test only one file**
|
#### Test only one file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest freqtrade/tests/test_<file_name>.py
|
pytest freqtrade/tests/test_<file_name>.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Test only one method from one file**
|
#### Test only one method from one file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
|
pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Test if your code is PEP8 compliant
|
### 2. Test if your code is PEP8 compliant
|
||||||
**Install packages** (If not already installed)
|
|
||||||
|
#### Install packages
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip3.6 install flake8 coveralls
|
pip3.6 install flake8 coveralls
|
||||||
```
|
```
|
||||||
**Run Flake8**
|
|
||||||
|
#### Run Flake8
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flake8 freqtrade
|
flake8 freqtrade
|
||||||
```
|
```
|
||||||
|
@ -47,16 +58,74 @@ 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.
|
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).
|
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||||
|
|
||||||
## 3. Test if all type-hints are correct
|
### 3. Test if all type-hints are correct
|
||||||
|
|
||||||
**Install packages** (If not already installed)
|
#### Install packages
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
pip3.6 install mypy
|
pip3.6 install mypy
|
||||||
```
|
```
|
||||||
|
|
||||||
**Run mypy**
|
#### Run mypy
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
mypy freqtrade
|
mypy freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started.
|
||||||
|
|
||||||
|
## (Core)-Committer Guide
|
||||||
|
|
||||||
|
### Process: Pull Requests
|
||||||
|
|
||||||
|
How to prioritize pull requests, from most to least important:
|
||||||
|
|
||||||
|
1. Fixes for broken tests. Broken means broken on any supported platform or Python version.
|
||||||
|
1. Extra tests to cover corner cases.
|
||||||
|
1. Minor edits to docs.
|
||||||
|
1. Bug fixes.
|
||||||
|
1. Major edits to docs.
|
||||||
|
1. Features.
|
||||||
|
|
||||||
|
Ensure that each pull request meets all requirements in the Contributing document.
|
||||||
|
|
||||||
|
### Process: Issues
|
||||||
|
|
||||||
|
If an issue is a bug that needs an urgent fix, mark it for the next patch release.
|
||||||
|
Then either fix it or mark as please-help.
|
||||||
|
|
||||||
|
For other issues: encourage friendly discussion, moderate debate, offer your thoughts.
|
||||||
|
|
||||||
|
### Process: Your own code changes
|
||||||
|
|
||||||
|
All code changes, regardless of who does them, need to be reviewed and merged by someone else.
|
||||||
|
This rule applies to all the core committers.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
|
||||||
|
- Minor corrections and fixes to pull requests submitted by others.
|
||||||
|
- While making a formal release, the release manager can make necessary, appropriate changes.
|
||||||
|
- Small documentation changes that reinforce existing subject matter. Most commonly being, but not limited to spelling and grammar corrections.
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
|
||||||
|
- Ensure cross-platform compatibility for every change that's accepted. Windows, Mac & Linux.
|
||||||
|
- Ensure no malicious code is introduced into the core code.
|
||||||
|
- Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback.
|
||||||
|
- Keep feature versions as small as possible, preferably one new feature per version.
|
||||||
|
- Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See the Python Community Code of Conduct (https://www.python.org/psf/codeofconduct/).
|
||||||
|
|
||||||
|
### Becoming a Committer
|
||||||
|
|
||||||
|
Contributors may be given commit privileges. Preference will be given to those with:
|
||||||
|
|
||||||
|
1. Past contributions to FreqTrade and other related open-source projects. Contributions to FreqTrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Quantity and quality are considered.
|
||||||
|
1. A coding style that the other core committers find simple, minimal, and clean.
|
||||||
|
1. Access to resources for cross-platform development and testing.
|
||||||
|
1. Time to devote to the project regularly.
|
||||||
|
|
||||||
|
Beeing a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust FreqTrade with their Exchange API keys).
|
||||||
|
|
||||||
|
After beeing Committer for some time, a Committer may be named Core Committer and given full repository access.
|
||||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -1,19 +1,20 @@
|
||||||
FROM python:3.7.0-slim-stretch
|
FROM python:3.7.0-slim-stretch
|
||||||
|
|
||||||
# Install TA-lib
|
RUN apt-get update \
|
||||||
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
&& apt-get -y install curl build-essential \
|
||||||
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
&& apt-get clean \
|
||||||
tar xzvf - && \
|
&& pip install --upgrade pip
|
||||||
cd ta-lib && \
|
|
||||||
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \
|
|
||||||
./configure && make && make install && \
|
|
||||||
cd .. && rm -rf ta-lib
|
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
|
||||||
|
|
||||||
# Prepare environment
|
# Prepare environment
|
||||||
RUN mkdir /freqtrade
|
RUN mkdir /freqtrade
|
||||||
WORKDIR /freqtrade
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
|
# Install TA-lib
|
||||||
|
COPY build_helpers/* /tmp/
|
||||||
|
RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
|
||||||
|
|
||||||
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt /freqtrade/
|
COPY requirements.txt /freqtrade/
|
||||||
RUN pip install numpy --no-cache-dir \
|
RUN pip install numpy --no-cache-dir \
|
||||||
|
|
9
Dockerfile.develop
Normal file
9
Dockerfile.develop
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
FROM freqtradeorg/freqtrade:develop
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements-dev.txt /freqtrade/
|
||||||
|
RUN pip install numpy --no-cache-dir \
|
||||||
|
&& pip install -r requirements-dev.txt --no-cache-dir
|
||||||
|
|
||||||
|
# Empty the ENTRYPOINT to allow all commands
|
||||||
|
ENTRYPOINT []
|
6
Dockerfile.technical
Normal file
6
Dockerfile.technical
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FROM freqtradeorg/freqtrade:develop
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get -y install git \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& pip install git+https://github.com/berlinguyinca/technical
|
12
README.md
12
README.md
|
@ -34,13 +34,15 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||||
- [x] **Dry-run**: Run the bot without playing money.
|
- [x] **Dry-run**: Run the bot without playing money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
||||||
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
|
||||||
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
|
||||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
|
@ -51,6 +53,7 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
||||||
|
- [Edge](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
||||||
- [Basic Usage](#basic-usage)
|
- [Basic Usage](#basic-usage)
|
||||||
- [Bot commands](#bot-commands)
|
- [Bot commands](#bot-commands)
|
||||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
- [Telegram RPC commands](#telegram-rpc-commands)
|
||||||
|
@ -62,6 +65,8 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Min hardware required](#min-hardware-required)
|
- [Min hardware required](#min-hardware-required)
|
||||||
- [Software requirements](#software-requirements)
|
- [Software requirements](#software-requirements)
|
||||||
|
- [Wanna help?](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
|
- [Dev - getting started](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP)
|
||||||
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
@ -190,10 +195,14 @@ in the bug reports.
|
||||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||||
|
|
||||||
Please read our
|
Please read our
|
||||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
to understand the requirements before sending your pull-requests.
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
|
Coding is not a neccessity to contribute - maybe start with improving our documentation?
|
||||||
|
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
|
||||||
|
|
||||||
**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.
|
**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`.
|
**Important:** Always create your PR against the `develop` branch, not `master`.
|
||||||
|
@ -218,3 +227,4 @@ To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
||||||
|
|
||||||
|
|
13
build_helpers/install_ta-lib.sh
Executable file
13
build_helpers/install_ta-lib.sh
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
|
cd ta-lib \
|
||||||
|
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
|
||||||
|
&& ./configure \
|
||||||
|
&& make \
|
||||||
|
&& which sudo && sudo make install || make install \
|
||||||
|
&& cd ..
|
||||||
|
else
|
||||||
|
echo "TA-lib already installed, skipping download and build."
|
||||||
|
cd ta-lib && sudo make install && cd ..
|
||||||
|
|
||||||
|
fi
|
60
build_helpers/publish_docker.sh
Executable file
60
build_helpers/publish_docker.sh
Executable file
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# - export TAG=`if [ "$TRAVIS_BRANCH" == "develop" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
|
||||||
|
# Replace / with _ to create a valid tag
|
||||||
|
TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/")
|
||||||
|
|
||||||
|
|
||||||
|
# Add commit and commit_message to docker container
|
||||||
|
echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit
|
||||||
|
|
||||||
|
if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then
|
||||||
|
echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache"
|
||||||
|
docker build -t freqtrade:${TAG} .
|
||||||
|
else
|
||||||
|
echo "event ${TRAVIS_EVENT_TYPE}: building with cache"
|
||||||
|
# Pull last build to avoid rebuilding the whole image
|
||||||
|
docker pull ${REPO}:${TAG}
|
||||||
|
docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} .
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed building image"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run backtest
|
||||||
|
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro freqtrade:${TAG} --datadir freqtrade/tests/testdata backtesting
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed running backtest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tag image for upload
|
||||||
|
docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed tagging image"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tag as latest for develop builds
|
||||||
|
if [ "${TRAVIS_BRANCH}" = "develop" ]; then
|
||||||
|
docker tag freqtrade:$TAG ${IMAGE_NAME}:latest
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Login
|
||||||
|
echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed login"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show all available images
|
||||||
|
docker images
|
||||||
|
|
||||||
|
docker push ${IMAGE_NAME}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "failed pushing repo"
|
||||||
|
return 1
|
||||||
|
fi
|
|
@ -28,7 +28,10 @@
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"ccxt_rate_limit": true,
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": false
|
||||||
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
"ETH/BTC",
|
||||||
"LTC/BTC",
|
"LTC/BTC",
|
||||||
|
@ -50,12 +53,28 @@
|
||||||
"sell_profit_only": false,
|
"sell_profit_only": false,
|
||||||
"ignore_roi_if_buy_signal": false
|
"ignore_roi_if_buy_signal": false
|
||||||
},
|
},
|
||||||
|
"edge": {
|
||||||
|
"enabled": false,
|
||||||
|
"process_throttle_secs": 3600,
|
||||||
|
"calculate_since_number_of_days": 7,
|
||||||
|
"capital_available_percentage": 0.5,
|
||||||
|
"allowed_risk": 0.01,
|
||||||
|
"stoploss_range_min": -0.01,
|
||||||
|
"stoploss_range_max": -0.1,
|
||||||
|
"stoploss_range_step": -0.01,
|
||||||
|
"minimum_winrate": 0.60,
|
||||||
|
"minimum_expectancy": 0.20,
|
||||||
|
"min_trade_number": 10,
|
||||||
|
"max_trade_duration_minute": 1440,
|
||||||
|
"remove_pumps": 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"
|
||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
}
|
}
|
||||||
|
|
83
config_binance.json.example
Normal file
83
config_binance.json.example
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"max_open_trades": 3,
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.05,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
|
"ticker_interval" : "5m",
|
||||||
|
"dry_run": true,
|
||||||
|
"trailing_stop": false,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_min": 1,
|
||||||
|
"order_book_max": 9
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": false
|
||||||
|
},
|
||||||
|
"pair_whitelist": [
|
||||||
|
"AST/BTC",
|
||||||
|
"ETC/BTC",
|
||||||
|
"ETH/BTC",
|
||||||
|
"EOS/BTC",
|
||||||
|
"IOTA/BTC",
|
||||||
|
"LTC/BTC",
|
||||||
|
"MTH/BTC",
|
||||||
|
"NCASH/BTC",
|
||||||
|
"TNT/BTC",
|
||||||
|
"XMR/BTC",
|
||||||
|
"XLM/BTC",
|
||||||
|
"XRP/BTC"
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"BNB/BTC"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"use_sell_signal": false,
|
||||||
|
"sell_profit_only": false,
|
||||||
|
"ignore_roi_if_buy_signal": false
|
||||||
|
},
|
||||||
|
"edge": {
|
||||||
|
"enabled": false,
|
||||||
|
"process_throttle_secs": 3600,
|
||||||
|
"calculate_since_number_of_days": 7,
|
||||||
|
"capital_available_percentage": 0.5,
|
||||||
|
"allowed_risk": 0.01,
|
||||||
|
"stoploss_range_min": -0.01,
|
||||||
|
"stoploss_range_max": -0.1,
|
||||||
|
"stoploss_range_step": -0.01,
|
||||||
|
"minimum_winrate": 0.60,
|
||||||
|
"minimum_expectancy": 0.20,
|
||||||
|
"min_trade_number": 10,
|
||||||
|
"max_trade_duration_minute": 1440,
|
||||||
|
"remove_pumps": false
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": false,
|
||||||
|
"token": "your_telegram_token",
|
||||||
|
"chat_id": "your_telegram_chat_id"
|
||||||
|
},
|
||||||
|
"initial_state": "running",
|
||||||
|
"forcebuy_enable": false,
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,11 +33,32 @@
|
||||||
"order_book_min": 1,
|
"order_book_min": 1,
|
||||||
"order_book_max": 9
|
"order_book_max": 9
|
||||||
},
|
},
|
||||||
|
"order_types": {
|
||||||
|
"buy": "limit",
|
||||||
|
"sell": "limit",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": "false"
|
||||||
|
},
|
||||||
|
"order_time_in_force": {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc",
|
||||||
|
},
|
||||||
|
"pairlist": {
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"config": {
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume"
|
||||||
|
}
|
||||||
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
"ccxt_rate_limit": true,
|
"ccxt_config": {"enableRateLimit": true},
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"enableRateLimit": false,
|
||||||
|
"aiohttp_trust_env": false
|
||||||
|
},
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
"ETH/BTC",
|
||||||
"LTC/BTC",
|
"LTC/BTC",
|
||||||
|
@ -55,6 +76,21 @@
|
||||||
],
|
],
|
||||||
"outdated_offset": 5
|
"outdated_offset": 5
|
||||||
},
|
},
|
||||||
|
"edge": {
|
||||||
|
"enabled": false,
|
||||||
|
"process_throttle_secs": 3600,
|
||||||
|
"calculate_since_number_of_days": 7,
|
||||||
|
"capital_available_percentage": 0.5,
|
||||||
|
"allowed_risk": 0.01,
|
||||||
|
"stoploss_range_min": -0.01,
|
||||||
|
"stoploss_range_max": -0.1,
|
||||||
|
"stoploss_range_step": -0.01,
|
||||||
|
"minimum_winrate": 0.60,
|
||||||
|
"minimum_expectancy": 0.20,
|
||||||
|
"min_trade_number": 10,
|
||||||
|
"max_trade_duration_minute": 1440,
|
||||||
|
"remove_pumps": false
|
||||||
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
"sell_profit_only": false,
|
"sell_profit_only": false,
|
||||||
|
@ -67,6 +103,7 @@
|
||||||
},
|
},
|
||||||
"db_url": "sqlite:///tradesv3.sqlite",
|
"db_url": "sqlite:///tradesv3.sqlite",
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
|
"forcebuy_enable": false,
|
||||||
"internals": {
|
"internals": {
|
||||||
"process_throttle_secs": 5
|
"process_throttle_secs": 5
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,18 @@ indicators.
|
||||||
|
|
||||||
- [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)
|
- [Anatomy of a strategy](#anatomy-of-a-strategy)
|
||||||
|
- [Customize indicators](#customize-indicators)
|
||||||
|
- [Buy signal rules](#buy-signal-rules)
|
||||||
|
- [Sell signal rules](#sell-signal-rules)
|
||||||
|
- [Minimal ROI](#minimal-roi)
|
||||||
|
- [Stoploss](#stoploss)
|
||||||
|
- [Ticker interval](#ticker-interval)
|
||||||
|
- [Metadata dict](#metadata-dict)
|
||||||
|
- [Where is the default strategy](#where-is-the-default-strategy)
|
||||||
|
- [Specify custom strategy location](#specify-custom-strategy-location)
|
||||||
|
- [Further strategy ideas](#further-strategy-ideas)
|
||||||
|
|
||||||
- [Where is the default strategy](#where-is-the-default-strategy)
|
- [Where is the default strategy](#where-is-the-default-strategy)
|
||||||
|
|
||||||
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.
|
||||||
|
@ -33,12 +44,18 @@ 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`.
|
||||||
|
|
||||||
|
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes.
|
||||||
|
`cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py`
|
||||||
|
|
||||||
|
### Anatomy of a strategy
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
- Indicators
|
||||||
- Buy strategy rules
|
- Buy strategy rules
|
||||||
- Sell strategy rules
|
- Sell strategy rules
|
||||||
- Minimal ROI recommended
|
- Minimal ROI recommended
|
||||||
- Stoploss recommended
|
- Stoploss strongly recommended
|
||||||
|
|
||||||
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`
|
||||||
|
@ -47,73 +64,17 @@ You can test it with the parameter: `--strategy TestStrategy`
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specify custom strategy location
|
|
||||||
|
|
||||||
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
### Buy strategy
|
### Customize Indicators
|
||||||
|
|
||||||
Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
|
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame populated with indicators
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: DataFrame with buy column
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 30) &
|
|
||||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sell strategy
|
|
||||||
|
|
||||||
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
|
||||||
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
|
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
|
||||||
:param dataframe: DataFrame populated with indicators
|
|
||||||
:param metadata: Additional information, like the currently traded pair
|
|
||||||
:return: DataFrame with buy column
|
|
||||||
"""
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 70) &
|
|
||||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
|
||||||
),
|
|
||||||
'sell'] = 1
|
|
||||||
return dataframe
|
|
||||||
```
|
|
||||||
|
|
||||||
## Add more Indicators
|
|
||||||
|
|
||||||
As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
|
||||||
|
|
||||||
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
|
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
|
||||||
|
|
||||||
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -157,21 +118,144 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Want more indicator examples
|
||||||
|
|
||||||
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||||
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
### Buy signal rules
|
||||||
|
|
||||||
|
Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy.
|
||||||
|
|
||||||
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
|
This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
|
||||||
|
|
||||||
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 30) &
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sell signal rules
|
||||||
|
|
||||||
|
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||||
|
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
|
||||||
|
|
||||||
|
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
|
||||||
|
|
||||||
|
This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
|
||||||
|
|
||||||
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimal ROI
|
||||||
|
|
||||||
|
This dict defines the minimal Return On Investment (ROI) a trade should reach before selling, independent from the sell signal.
|
||||||
|
|
||||||
|
It is of the following format, with the dict key (left side of the colon) being the minutes passed since the trade opened, and the value (right side of the colon) being the percentage.
|
||||||
|
|
||||||
|
```python
|
||||||
|
minimal_roi = {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above configuration would therefore mean:
|
||||||
|
|
||||||
|
- Sell whenever 4% profit was reached
|
||||||
|
- Sell after 20 minutes when 2% profit was reached
|
||||||
|
- Sell after 20 minutes when 2% profit was reached
|
||||||
|
- Sell after 30 minutes when 1% profit was reached
|
||||||
|
- Sell after 40 minutes when the trade is non-loosing (no profit)
|
||||||
|
|
||||||
|
The calculation does include fees.
|
||||||
|
|
||||||
|
To disable ROI completely, set it to an insanely high number:
|
||||||
|
|
||||||
|
```python
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While technically not completely disabled, this would sell once the trade reaches 10000% Profit.
|
||||||
|
|
||||||
|
### Stoploss
|
||||||
|
|
||||||
|
Setting a stoploss is highly recommended to protect your capital from strong moves against you.
|
||||||
|
|
||||||
|
Sample:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
stoploss = -0.10
|
||||||
|
```
|
||||||
|
|
||||||
|
This would signify a stoploss of -10%.
|
||||||
|
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems).
|
||||||
|
|
||||||
|
For more information on order_types please look [here](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md#understand-order_types).
|
||||||
|
|
||||||
|
### Ticker interval
|
||||||
|
|
||||||
|
This is the set of candles the bot should download and use for the analysis.
|
||||||
|
Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
|
||||||
|
|
||||||
|
Please note that the same buy/sell signals may work with one interval, but not the other.
|
||||||
|
|
||||||
### Metadata dict
|
### Metadata dict
|
||||||
|
|
||||||
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
||||||
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||||
|
|
||||||
### Want more indicator examples
|
|
||||||
|
|
||||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
|
||||||
Then uncomment indicators you need.
|
|
||||||
|
|
||||||
### Where is the default strategy?
|
### Where is the default strategy?
|
||||||
|
|
||||||
The default buy strategy is located in the file
|
The default buy strategy is located in the file
|
||||||
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||||
|
|
||||||
|
### Specify custom strategy location
|
||||||
|
|
||||||
|
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
||||||
|
```
|
||||||
|
|
||||||
### Further strategy ideas
|
### 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.
|
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.
|
||||||
|
|
|
@ -36,7 +36,7 @@ optional arguments:
|
||||||
--strategy-path PATH specify additional strategy lookup path
|
--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)
|
BaseVolume (default: 20) DEPRECATED
|
||||||
--db-url PATH Override trades database URL, this is useful if
|
--db-url PATH Override trades database URL, this is useful if
|
||||||
dry_run is enabled or in custom deployments (default:
|
dry_run is enabled or in custom deployments (default:
|
||||||
sqlite:///tradesv3.sqlite)
|
sqlite:///tradesv3.sqlite)
|
||||||
|
@ -89,6 +89,8 @@ This is very simple. Copy paste your strategy file into the folder
|
||||||
|
|
||||||
### How to use --dynamic-whitelist?
|
### How to use --dynamic-whitelist?
|
||||||
|
|
||||||
|
> Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](docs/configuration.md#Dynamic-Pairlists)
|
||||||
|
|
||||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||||
on BaseVolume. This value can be changed when you run the script.
|
on BaseVolume. This value can be changed when you run the script.
|
||||||
|
|
||||||
|
@ -204,6 +206,8 @@ optional arguments:
|
||||||
number)
|
number)
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
specify what timerange of data to use.
|
specify what timerange of data to use.
|
||||||
|
--hyperopt PATH specify hyperopt file (default:
|
||||||
|
freqtrade/optimize/default_hyperopt.py)
|
||||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||||
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
||||||
Specify which parameters to hyperopt. Space separate
|
Specify which parameters to hyperopt. Space separate
|
||||||
|
@ -211,6 +215,33 @@ optional arguments:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Edge commands
|
||||||
|
|
||||||
|
To know your trade expectacny and winrate against historical data, you can use Edge.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: main.py edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] [-r]
|
||||||
|
[--stoplosses STOPLOSS_RANGE]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
|
-r, --refresh-pairs-cached
|
||||||
|
refresh the pairs files in tests/testdata with the
|
||||||
|
latest data from the exchange. Use it if you want to
|
||||||
|
run your edge with up-to-date data.
|
||||||
|
--stoplosses STOPLOSS_RANGE
|
||||||
|
defines a range of stoploss against which edge will
|
||||||
|
assess the strategythe format is "min,max,step"
|
||||||
|
(without any space).example:
|
||||||
|
--stoplosses=-0.01,-0.1,-0.001
|
||||||
|
```
|
||||||
|
|
||||||
|
To understand edge and how to read the results, please read the [edge documentation](edge.md).
|
||||||
|
|
||||||
## 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
|
||||||
|
|
|
@ -17,7 +17,7 @@ The table below will list all configuration parameters.
|
||||||
|
|
||||||
| Command | Default | Mandatory | Description |
|
| Command | Default | Mandatory | Description |
|
||||||
|----------|---------|----------|-------------|
|
|----------|---------|----------|-------------|
|
||||||
| `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. If -1 then it is ignored (i.e. potentially unlimited open trades)
|
||||||
| `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. Set it to 'unlimited' to allow the bot to use all avaliable balance.
|
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
|
||||||
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
| `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
|
||||||
|
@ -39,25 +39,33 @@ The table below will list all configuration parameters.
|
||||||
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||||
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
|
| `order_types` | None | No | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types).
|
||||||
|
| `order_time_in_force` | None | No | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force).
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.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.
|
||||||
| `exchange.ccxt_rate_limit` | True | No | Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
| `exchange.ccxt_rate_limit` | True | No | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
||||||
|
| `exchange.ccxt_config` | None | No | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||||
|
| `exchange.ccxt_async_config` | None | No | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
|
||||||
|
| `edge` | false | No | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
| `experimental.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`
|
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
||||||
|
| `pairlist.method` | StaticPairList | No | Use Static whitelist. [More information below](#dynamic-pairlists).
|
||||||
|
| `pairlist.config` | None | No | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
|
||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
| `telegram.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`.
|
||||||
| `webhook.enabled` | false | No | Enable useage of Webhook notifications
|
| `webhook.enabled` | false | No | Enable usage of Webhook notifications
|
||||||
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
||||||
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
| `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.
|
||||||
|
| `forcebuy_enable` | false | No | Enables the RPC Commands to force a buy. 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.
|
||||||
|
@ -111,6 +119,15 @@ Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing
|
||||||
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 forcebuy_enable
|
||||||
|
|
||||||
|
`forcebuy_enable` enables the usage of forcebuy commands via Telegram.
|
||||||
|
This is disabled for security reasons by default, and will show a warning message on startup if enabled.
|
||||||
|
You send `/forcebuy ETH/BTC` to the bot, who buys the pair and holds it until a regular sell-signal appears (ROI, stoploss, /forcesell).
|
||||||
|
|
||||||
|
Can be dangerous with some strategies, so use with care
|
||||||
|
See [the telegram documentation](telegram-usage.md) for details on usage.
|
||||||
|
|
||||||
### Understand process_throttle_secs
|
### Understand process_throttle_secs
|
||||||
|
|
||||||
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||||
|
@ -125,6 +142,45 @@ 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.
|
||||||
|
|
||||||
|
### Understand order_types
|
||||||
|
|
||||||
|
`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled.
|
||||||
|
This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.
|
||||||
|
|
||||||
|
If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start.
|
||||||
|
The below is the default which is used if this is not configured in either Strategy or configuration.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
"order_types": {
|
||||||
|
"buy": "limit",
|
||||||
|
"sell": "limit",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE**: Not all exchanges support "market" orders.
|
||||||
|
The following message will be shown if your exchange does not support market orders: `"Exchange <yourexchange> does not support market orders."`
|
||||||
|
|
||||||
|
### Understand order_time_in_force
|
||||||
|
Order time in force defines the policy by which the order is executed on the exchange. Three commonly used time in force are:<br/>
|
||||||
|
**GTC (Goog Till Canceled):**
|
||||||
|
This is most of the time the default time in force. It means the order will remain on exchange till it is canceled by user. It can be fully or partially fulfilled. If partially fulfilled, the remaining will stay on the exchange till cancelled.<br/>
|
||||||
|
**FOK (Full Or Kill):**
|
||||||
|
It means if the order is not executed immediately AND fully then it is canceled by the exchange.<br/>
|
||||||
|
**IOC (Immediate Or Canceled):**
|
||||||
|
It is the same as FOK (above) except it can be partially fulfilled. The remaining part is automatically cancelled by the exchange.
|
||||||
|
<br/>
|
||||||
|
`order_time_in_force` contains a dict buy and sell time in force policy. This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations.<br/>
|
||||||
|
possible values are: `gtc` (default), `fok` or `ioc`.<br/>
|
||||||
|
``` python
|
||||||
|
"order_time_in_force": {
|
||||||
|
"buy": "gtc",
|
||||||
|
"sell": "gtc"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
**NOTE**: This is an ongoing work. For now it is supported only for binance and only for buy orders. Please don't change the default value unless you know what you are doing.<br/>
|
||||||
|
|
||||||
### What values for exchange.name?
|
### What values for exchange.name?
|
||||||
|
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
|
@ -177,13 +233,38 @@ creating trades.
|
||||||
Once you will be happy with your bot performance, you can switch it to
|
Once you will be happy with your bot performance, you can switch it to
|
||||||
production mode.
|
production mode.
|
||||||
|
|
||||||
|
### Dynamic Pairlists
|
||||||
|
|
||||||
|
Dynamic pairlists select pairs for you based on the logic configured.
|
||||||
|
The bot runs against all pairs (with that stake) on the exchange, and a number of assets (`number_assets`) is selected based on the selected criteria.
|
||||||
|
|
||||||
|
By *default*, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration).
|
||||||
|
|
||||||
|
#### Available Pairlist methods
|
||||||
|
|
||||||
|
* `"StaticPairList"`
|
||||||
|
* uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`
|
||||||
|
* `"VolumePairList"`
|
||||||
|
* Formerly available as `--dynamic-whitelist [<number_assets>]`
|
||||||
|
* Selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlist": {
|
||||||
|
"method": "VolumePairList",
|
||||||
|
"config": {
|
||||||
|
"number_assets": 20,
|
||||||
|
"sort_key": "quoteVolume"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
## Switch to production mode
|
## Switch to production mode
|
||||||
|
|
||||||
In production mode, the bot will engage your money. Be careful a wrong
|
In production mode, the bot will engage your money. Be careful a wrong
|
||||||
strategy can lose all your money. Be aware of what you are doing when
|
strategy can lose all your money. Be aware of what you are doing when
|
||||||
you run it in production mode.
|
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
|
||||||
|
|
||||||
|
@ -204,8 +285,29 @@ you run it in production mode.
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||||
|
|
||||||
|
### Using proxy with FreqTrade
|
||||||
|
|
||||||
|
To use a proxy with freqtrade, add the kwarg `"aiohttp_trust_env"=true` to the `"ccxt_async_kwargs"` dict in the exchange section of the configuration.
|
||||||
|
|
||||||
|
An example for this can be found in `config_full.json.example`
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"ccxt_async_config": {
|
||||||
|
"aiohttp_trust_env": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
export HTTP_PROXY="http://addr:port"
|
||||||
|
export HTTPS_PROXY="http://addr:port"
|
||||||
|
freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Embedding Strategies
|
### Embedding Strategies
|
||||||
|
|
||||||
|
@ -213,7 +315,7 @@ FreqTrade provides you with with an easy way to embed the strategy into your con
|
||||||
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
|
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
|
||||||
in your chosen config file.
|
in your chosen config file.
|
||||||
|
|
||||||
##### Encoding a string as BASE64
|
#### Encoding a string as BASE64
|
||||||
|
|
||||||
This is a quick example, how to generate the BASE64 string in python
|
This is a quick example, how to generate the BASE64 string in python
|
||||||
|
|
||||||
|
|
70
docs/developer.md
Normal file
70
docs/developer.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Development Help
|
||||||
|
|
||||||
|
This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running.
|
||||||
|
|
||||||
|
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions.
|
||||||
|
|
||||||
|
|
||||||
|
## Module
|
||||||
|
|
||||||
|
### Dynamic Pairlist
|
||||||
|
|
||||||
|
You have a great idea for a new pair selection algorithm you would like to try out? Great.
|
||||||
|
Hopefully you also want to contribute this back upstream.
|
||||||
|
|
||||||
|
Whatever your motivations are - This should get you off the ground in trying to develop a new Pairlist provider.
|
||||||
|
|
||||||
|
First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/pairlist/VolumePairList.py) provider, and best copy this file with a name of your new Pairlist Provider.
|
||||||
|
|
||||||
|
This is a simple provider, which however serves as a good example on how to start developing.
|
||||||
|
|
||||||
|
Next, modify the classname of the provider (ideally align this with the Filename).
|
||||||
|
|
||||||
|
The base-class provides the an instance of the bot (`self._freqtrade`), as well as the configuration (`self._config`), and initiates both `_blacklist` and `_whitelist`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
self._freqtrade = freqtrade
|
||||||
|
self._config = config
|
||||||
|
self._whitelist = self._config['exchange']['pair_whitelist']
|
||||||
|
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Now, let's step through the methods which require actions:
|
||||||
|
|
||||||
|
#### configuration
|
||||||
|
|
||||||
|
Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`.
|
||||||
|
This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist.
|
||||||
|
By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience.
|
||||||
|
|
||||||
|
Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic.
|
||||||
|
|
||||||
|
#### short_desc
|
||||||
|
|
||||||
|
Returns a description used for Telegram messages.
|
||||||
|
This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`.
|
||||||
|
|
||||||
|
#### refresh_pairlist
|
||||||
|
|
||||||
|
Override this method and run all calculations needed in this method.
|
||||||
|
This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations.
|
||||||
|
|
||||||
|
Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly.
|
||||||
|
|
||||||
|
Please also run `self._validate_whitelist(pairs)` and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten.
|
||||||
|
|
||||||
|
##### sample
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def refresh_pairlist(self) -> None:
|
||||||
|
# Generate dynamic whitelist
|
||||||
|
pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key)
|
||||||
|
# Validate whitelist to only have active market pairs
|
||||||
|
self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### _gen_pair_whitelist
|
||||||
|
|
||||||
|
This is a simple method used by `VolumePairList` - however serves as a good example.
|
||||||
|
It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider.
|
216
docs/edge.md
Normal file
216
docs/edge.md
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
# Edge positioning
|
||||||
|
|
||||||
|
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
|
||||||
|
|
||||||
|
**NOTICE:** Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist.
|
||||||
|
**NOTICE2:** Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Introduction](#introduction)
|
||||||
|
- [How does it work?](#how-does-it-work?)
|
||||||
|
- [Configurations](#configurations)
|
||||||
|
- [Running Edge independently](#running-edge-independently)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/>
|
||||||
|
But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: You give me 10$. Is it an interesting game ? no, it is quite boring, isn't it?<br/><br/>
|
||||||
|
But let's say the probability that we have heads is 80%, and the probability that we have tails is 20%. Now it is becoming interesting ...
|
||||||
|
That means 10$ x 80% versus 10$ x 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin.<br/><br/>
|
||||||
|
Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% * 2$ versus 20% * 8$. It is becoming boring again because overtime you win $1.6$ (80% x 2$) and me $1.6 (20% * 8$) too.<br/><br/>
|
||||||
|
The question is: How do you calculate that? how do you know if you wanna play?
|
||||||
|
The answer comes to two factors:
|
||||||
|
- Win Rate
|
||||||
|
- Risk Reward Ratio
|
||||||
|
|
||||||
|
|
||||||
|
### Win Rate
|
||||||
|
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
|
||||||
|
|
||||||
|
|
||||||
|
`W = (Number of winning trades) / (Number of losing trades)`
|
||||||
|
|
||||||
|
### Risk Reward Ratio
|
||||||
|
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
|
||||||
|
|
||||||
|
`R = Profit / Loss`
|
||||||
|
|
||||||
|
Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades:
|
||||||
|
|
||||||
|
`Average profit = (Sum of profits) / (Number of winning trades)`
|
||||||
|
|
||||||
|
`Average loss = (Sum of losses) / (Number of losing trades)`
|
||||||
|
|
||||||
|
`R = (Average profit) / (Average loss)`
|
||||||
|
|
||||||
|
### Expectancy
|
||||||
|
|
||||||
|
At this point we can combine W and R to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades, and subtracting the percentage of losing trades, which is calculated as follows:
|
||||||
|
|
||||||
|
Expectancy Ratio = (Risk Reward Ratio x Win Rate) – Loss Rate
|
||||||
|
|
||||||
|
So lets say your Win rate is 28% and your Risk Reward Ratio is 5:
|
||||||
|
|
||||||
|
`Expectancy = (5 * 0.28) - 0.72 = 0.68`
|
||||||
|
|
||||||
|
Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your losers. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||||
|
|
||||||
|
It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future.
|
||||||
|
|
||||||
|
You can also use this number to evaluate the effectiveness of modifications to this system.
|
||||||
|
|
||||||
|
**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data , there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||||
|
|
||||||
|
## How does it work?
|
||||||
|
If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over X trades for each stoploss. Here is an example:
|
||||||
|
|
||||||
|
| Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy |
|
||||||
|
|----------|:-------------:|-------------:|------------------:|-----------:|
|
||||||
|
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||||
|
| XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 |
|
||||||
|
| XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 |
|
||||||
|
|
||||||
|
The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at 3% leads to the maximum expectancy according to historical data.
|
||||||
|
|
||||||
|
Edge then forces stoploss to your strategy dynamically.
|
||||||
|
|
||||||
|
### Position size
|
||||||
|
Edge dictates the stake amount for each trade to the bot according to the following factors:
|
||||||
|
|
||||||
|
- Allowed capital at risk
|
||||||
|
- Stoploss
|
||||||
|
|
||||||
|
Allowed capital at risk is calculated as follows:
|
||||||
|
|
||||||
|
**allowed capital at risk** = **capital_available_percentage** X **allowed risk per trade**
|
||||||
|
|
||||||
|
**Stoploss** is calculated as described above against historical data.
|
||||||
|
|
||||||
|
Your position size then will be:
|
||||||
|
|
||||||
|
**position size** = **allowed capital at risk** / **stoploss**
|
||||||
|
|
||||||
|
Example:<br/>
|
||||||
|
Let's say the stake currency is ETH and you have 10 ETH on the exchange, your **capital_available_percentage** is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. <br/>
|
||||||
|
Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5ETH**.<br/>
|
||||||
|
Bot takes a position of 2.5ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on BTC/ETH market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25ETH (call it trade 2).<br/>
|
||||||
|
Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.<br/>
|
||||||
|
Now you have two trades open. The Bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5ETH**. But there are already 4ETH blocked in two previous trades. So the position size for this third trade would be 1ETH.<br/>
|
||||||
|
Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5ETH. <br/>
|
||||||
|
So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75**.
|
||||||
|
|
||||||
|
## Configurations
|
||||||
|
Edge has following configurations:
|
||||||
|
|
||||||
|
#### enabled
|
||||||
|
If true, then Edge will run periodically.<br/>
|
||||||
|
(default to false)
|
||||||
|
|
||||||
|
#### process_throttle_secs
|
||||||
|
How often should Edge run in seconds? <br/>
|
||||||
|
(default to 3600 so one hour)
|
||||||
|
|
||||||
|
#### calculate_since_number_of_days
|
||||||
|
Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy
|
||||||
|
Note that it downloads historical data so increasing this number would lead to slowing down the bot.<br/>
|
||||||
|
(default to 7)
|
||||||
|
|
||||||
|
#### capital_available_percentage
|
||||||
|
This is the percentage of the total capital on exchange in stake currency. <br/>
|
||||||
|
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.<br/>
|
||||||
|
(default to 0.5)
|
||||||
|
|
||||||
|
#### allowed_risk
|
||||||
|
Percentage of allowed risk per trade.<br/>
|
||||||
|
(default to 0.01 [1%])
|
||||||
|
|
||||||
|
#### stoploss_range_min
|
||||||
|
Minimum stoploss.<br/>
|
||||||
|
(default to -0.01)
|
||||||
|
|
||||||
|
#### stoploss_range_max
|
||||||
|
Maximum stoploss.<br/>
|
||||||
|
(default to -0.10)
|
||||||
|
|
||||||
|
#### stoploss_range_step
|
||||||
|
As an example if this is set to -0.01 then Edge will test the strategy for [-0.01, -0,02, -0,03 ..., -0.09, -0.10] ranges.
|
||||||
|
Note than having a smaller step means having a bigger range which could lead to slow calculation. <br/>
|
||||||
|
if you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br/>
|
||||||
|
(default to -0.01)
|
||||||
|
|
||||||
|
#### minimum_winrate
|
||||||
|
It filters pairs which don't have at least minimum_winrate.
|
||||||
|
This comes handy if you want to be conservative and don't comprise win rate in favor of risk reward ratio.<br/>
|
||||||
|
(default to 0.60)
|
||||||
|
|
||||||
|
#### minimum_expectancy
|
||||||
|
It filters paris which have an expectancy lower than this number .
|
||||||
|
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.<br/>
|
||||||
|
(default to 0.20)
|
||||||
|
|
||||||
|
#### min_trade_number
|
||||||
|
When calculating W and R and E (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br/>
|
||||||
|
(default to 10, it is highly recommended not to decrease this number)
|
||||||
|
|
||||||
|
#### max_trade_duration_minute
|
||||||
|
Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br/>
|
||||||
|
**NOTICE:** While configuring this value, you should take into consideration your ticker interval. as an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. default value is set assuming your strategy interval is relatively small (1m or 5m, etc).<br/>
|
||||||
|
(default to 1 day, 1440 = 60 * 24)
|
||||||
|
|
||||||
|
#### remove_pumps
|
||||||
|
Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br/>
|
||||||
|
(default to false)
|
||||||
|
|
||||||
|
|
||||||
|
## Running Edge independently
|
||||||
|
You can run Edge independently in order to see in details the result. Here is an example:
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py edge
|
||||||
|
```
|
||||||
|
|
||||||
|
An example of its output:
|
||||||
|
|
||||||
|
| pair | stoploss | win rate | risk reward ratio | required risk reward | expectancy | total number of trades | average duration (min) |
|
||||||
|
|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-------------------------:|-------------------------:|
|
||||||
|
| AGI/BTC | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 |
|
||||||
|
| NXS/BTC | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 |
|
||||||
|
| LEND/BTC | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 |
|
||||||
|
| VIA/BTC | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 |
|
||||||
|
| MTH/BTC | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 |
|
||||||
|
| ARDR/BTC | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 |
|
||||||
|
| BCPT/BTC | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 |
|
||||||
|
| WINGS/BTC | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 |
|
||||||
|
| VIBE/BTC | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 |
|
||||||
|
| MCO/BTC | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 |
|
||||||
|
| GNT/BTC | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 |
|
||||||
|
| HOT/BTC | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 |
|
||||||
|
| SNM/BTC | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 |
|
||||||
|
| APPC/BTC | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 |
|
||||||
|
| NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 |
|
||||||
|
|
||||||
|
### Update cached pairs with the latest data
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py edge --refresh-pairs-cached
|
||||||
|
```
|
||||||
|
|
||||||
|
### Precising stoploss range
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced use of timerange
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py edge --timerange=20181110-20181113
|
||||||
|
```
|
||||||
|
|
||||||
|
Doing --timerange=-200 will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop.
|
||||||
|
|
||||||
|
The full timerange specification:
|
||||||
|
|
||||||
|
* Use last 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 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
|
|
@ -1,4 +1,5 @@
|
||||||
# Hyperopt
|
# Hyperopt
|
||||||
|
|
||||||
This page explains how to tune your strategy by finding the optimal
|
This page explains how to tune your strategy by finding the optimal
|
||||||
parameters, a process called hyperparameter optimization. The bot uses several
|
parameters, a process called hyperparameter optimization. The bot uses several
|
||||||
algorithms included in the `scikit-optimize` package to accomplish this. The
|
algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||||
|
@ -8,25 +9,37 @@ and still take a long time.
|
||||||
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
- [Prepare your Hyperopt](#prepare-hyperopt)
|
||||||
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
||||||
- [Solving a Mystery](#solving-a-mystery)
|
- [Solving a Mystery](#solving-a-mystery)
|
||||||
- [Adding New Indicators](#adding-new-indicators)
|
- [Adding New Indicators](#adding-new-indicators)
|
||||||
- [Execute Hyperopt](#execute-hyperopt)
|
- [Execute Hyperopt](#execute-hyperopt)
|
||||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
- [Understand the hyperopt result](#understand-the-hyperopt-result)
|
||||||
|
|
||||||
## Prepare Hyperopting
|
## Prepare Hyperopting
|
||||||
We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
|
||||||
|
|
||||||
### Configure your Guards and Triggers
|
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||||
There are two places you need to change to add a new buy strategy for testing:
|
an example hyperopt file located into [user_data/hyperopts/](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py)
|
||||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
|
|
||||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
|
|
||||||
and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
|
|
||||||
|
|
||||||
There you have two different type of indicators: 1. `guards` and 2. `triggers`.
|
### 1. Install a Custom Hyperopt File
|
||||||
1. Guards are conditions like "never buy if ADX < 10", or "never buy if
|
This is very simple. Put your hyperopt file into the folder
|
||||||
current price is over EMA10".
|
`user_data/hyperopts`.
|
||||||
|
|
||||||
|
Let assume you want a hyperopt file `awesome_hyperopt.py`:
|
||||||
|
1. Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
|
||||||
|
|
||||||
|
|
||||||
|
### 2. Configure your Guards and Triggers
|
||||||
|
There are two places you need to change in your hyperopt file to add a
|
||||||
|
new buy hyperopt for testing:
|
||||||
|
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L230-L251).
|
||||||
|
- Inside [indicator_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/test_hyperopt.py#L207-L223).
|
||||||
|
|
||||||
|
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
|
|
||||||
|
1. Guards are conditions like "never buy if ADX < 10", or never buy if
|
||||||
|
current price is over EMA10.
|
||||||
2. Triggers are ones that actually trigger buy in specific moment, like
|
2. Triggers are ones that actually trigger buy in specific moment, like
|
||||||
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
||||||
bollinger band".
|
bollinger band".
|
||||||
|
@ -113,33 +126,40 @@ When you want to test an indicator that isn't used by the bot currently, remembe
|
||||||
add it to the `populate_indicators()` method in `hyperopt.py`.
|
add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||||
|
|
||||||
## Execute Hyperopt
|
## Execute Hyperopt
|
||||||
Once you have updated your hyperopt configuration you can run it.
|
|
||||||
Because hyperopt tries a lot of combination to find the best parameters
|
|
||||||
it will take time you will have the result (more than 30 mins).
|
|
||||||
|
|
||||||
We strongly recommend to use `screen` to prevent any connection loss.
|
Once you have updated your hyperopt configuration you can run it.
|
||||||
|
Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins).
|
||||||
|
|
||||||
|
We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
|
python3 ./freqtrade/main.py -s <strategyname> --hyperopt <hyperoptname> -c config.json hyperopt -e 5000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `<strategyname>` and `<hyperoptname>` as the names of the custom strategy
|
||||||
|
(only required for generating sells) and the custom hyperopt used.
|
||||||
|
|
||||||
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||||
running at least several thousand evaluations.
|
running at least several thousand evaluations.
|
||||||
|
|
||||||
### Execute Hyperopt with Different Ticker-Data Source
|
### Execute Hyperopt with Different Ticker-Data Source
|
||||||
|
|
||||||
If you would like to hyperopt parameters using an alternate ticker data that
|
If you would like to hyperopt parameters using an alternate ticker data that
|
||||||
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||||
use data from directory `user_data/data`.
|
use data from directory `user_data/data`.
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Testset
|
### Running Hyperopt with Smaller Testset
|
||||||
Use the `--timeperiod` 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 hyperopt --timeperiod -200
|
python3 ./freqtrade/main.py hyperopt --timerange -200
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
|
|
||||||
Use the `--spaces` argument to limit the search space used by hyperopt.
|
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||||
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||||
might make more sense to start by just searching for initial buy algorithm.
|
might make more sense to start by just searching for initial buy algorithm.
|
||||||
|
@ -154,7 +174,8 @@ 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`
|
||||||
|
|
||||||
## Understand the Hyperopts Result
|
## Understand the Hyperopt Result
|
||||||
|
|
||||||
Once Hyperopt is completed you can use the result to create a new strategy.
|
Once Hyperopt is completed you can use the result to create a new strategy.
|
||||||
Given the following result from hyperopt:
|
Given the following result from hyperopt:
|
||||||
|
|
||||||
|
@ -166,6 +187,7 @@ with values:
|
||||||
```
|
```
|
||||||
|
|
||||||
You should understand this result like:
|
You should understand this result like:
|
||||||
|
|
||||||
- The buy trigger that worked best was `bb_lower`.
|
- The buy trigger that worked best was `bb_lower`.
|
||||||
- You should not use ADX because `adx-enabled: False`)
|
- You should not use ADX because `adx-enabled: False`)
|
||||||
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
||||||
|
@ -173,15 +195,16 @@ You should understand this result like:
|
||||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||||
method, what those values match to.
|
method, what those values match to.
|
||||||
|
|
||||||
So for example you had `rsi-value: 29.0` so we would look
|
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
||||||
at `rsi`-block, that translates to the following code block:
|
|
||||||
```
|
```
|
||||||
(dataframe['rsi'] < 29.0)
|
(dataframe['rsi'] < 29.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
Translating your whole hyperopt result as the new buy-signal
|
Translating your whole hyperopt result as the new buy-signal
|
||||||
would then look like:
|
would then look like:
|
||||||
```
|
|
||||||
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
|
@ -192,6 +215,39 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Understand Hyperopt ROI results
|
||||||
|
|
||||||
|
If you are optimizing ROI, you're result will look as follows and include a ROI table.
|
||||||
|
|
||||||
|
```
|
||||||
|
Best result:
|
||||||
|
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||||
|
with values:
|
||||||
|
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower', 'roi_t1': 40, 'roi_t2': 57, 'roi_t3': 21, 'roi_p1': 0.03634636907306948, 'roi_p2': 0.055237357937802885, 'roi_p3': 0.015163796015548354, 'stoploss': -0.37996664668703606}
|
||||||
|
ROI table:
|
||||||
|
{0: 0.10674752302642071, 21: 0.09158372701087236, 78: 0.03634636907306948, 118: 0}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would translate to the following ROI table:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
minimal_roi = {
|
||||||
|
"118": 0,
|
||||||
|
"78": 0.0363463,
|
||||||
|
"21": 0.0915,
|
||||||
|
"0": 0.106
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate backtest result
|
||||||
|
|
||||||
|
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
|
||||||
|
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flag `--disable-max-market-positions`.
|
||||||
|
This setting is the default for hyperopt for speed reasons. You can overwrite this in the configuration by setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L283).
|
||||||
|
|
||||||
|
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
|
||||||
|
|
||||||
## Next Step
|
## Next Step
|
||||||
|
|
||||||
Now you have a perfect bot and want to control it from Telegram. Your
|
Now you have a perfect bot and want to control it from Telegram. Your
|
||||||
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||||
|
|
|
@ -21,10 +21,12 @@ Pull-request. Do not hesitate to reach us on
|
||||||
- [Bot commands](https://github.com/freqtrade/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/freqtrade/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/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||||
|
- [Edge commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#edge-commands)
|
||||||
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
|
||||||
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
|
||||||
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
|
- [Edge positioning](https://github.com/freqtrade/freqtrade/blob/develop/docs/edge.md)
|
||||||
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
||||||
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
|
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
|
||||||
|
@ -33,4 +35,5 @@ Pull-request. Do not hesitate to reach us on
|
||||||
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
||||||
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
||||||
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md))
|
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
||||||
|
- [Developer Docs](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md)
|
||||||
|
|
|
@ -109,13 +109,37 @@ Dry-Run
|
||||||
touch tradesv3.dryrun.sqlite
|
touch tradesv3.dryrun.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Build the Docker image
|
### 2. Download or build the docker image
|
||||||
|
|
||||||
|
Either use the prebuilt image from docker hub - or build the image yourself if you would like more control on which version is used.
|
||||||
|
|
||||||
|
Branches / tags available can be checked out on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/).
|
||||||
|
|
||||||
|
#### 2.1. Download the docker image
|
||||||
|
|
||||||
|
Pull the image from docker hub and (optionally) change the name of the image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull freqtradeorg/freqtrade:develop
|
||||||
|
# Optionally tag the repository so the run-commands remain shorter
|
||||||
|
docker tag freqtradeorg/freqtrade:develop freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
To update the image, simply run the above commands again and restart your running container.
|
||||||
|
|
||||||
|
#### 2.2. Build the Docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
docker build -t freqtrade .
|
docker build -t freqtrade .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f ./Dockerfile.develop -t freqtrade-dev .
|
||||||
|
```
|
||||||
|
|
||||||
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
|
||||||
|
@ -236,22 +260,27 @@ sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential auto
|
||||||
|
|
||||||
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
|
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
|
||||||
|
|
||||||
The following assumes that miniconda3 is installed and available in your environment, and is installed.
|
The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation.
|
||||||
It's recommended to use (mini)conda for this as installation/compilation of `scipy` and `pandas` takes a long time.
|
It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time.
|
||||||
|
If you have installed it from (mini)conda, you can remove `numpy`, `scipy`, and `pandas` from `requirements.txt` before you install it with `pip`.
|
||||||
|
|
||||||
|
Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot).
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
conda config --add channels rpi
|
conda config --add channels rpi
|
||||||
conda install python=3.6
|
conda install python=3.6
|
||||||
conda create -n freqtrade python=3.6
|
conda create -n freqtrade python=3.6
|
||||||
conda install scipy pandas
|
conda activate freqtrade
|
||||||
|
conda install scipy pandas numpy
|
||||||
|
|
||||||
pip install -r requirements.txt
|
sudo apt install libffi-dev
|
||||||
pip install -e .
|
python3 -m pip install -r requirements.txt
|
||||||
|
python3 -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### MacOS
|
### MacOS
|
||||||
|
|
||||||
#### Install Python 3.6, git, wget and ta-lib
|
#### Install Python 3.6, git and wget
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install python3 git wget
|
brew install python3 git wget
|
||||||
|
@ -268,9 +297,9 @@ wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||||
tar xvzf ta-lib-0.4.0-src.tar.gz
|
tar xvzf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib
|
cd ta-lib
|
||||||
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||||
./configure --prefix=/usr
|
./configure --prefix=/usr/local
|
||||||
make
|
make
|
||||||
make install
|
sudo make install
|
||||||
cd ..
|
cd ..
|
||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
|
@ -48,4 +48,4 @@ Both values can be configured in the main configuration file and requires `"trai
|
||||||
|
|
||||||
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
|
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
|
||||||
|
|
||||||
You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
You should also make sure to have this value (`trailing_stop_positive_offset`) lower than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
||||||
|
|
|
@ -23,6 +23,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||||
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||||
|
| `/forcebuy <pair> [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||||
| `/performance` | | Show performance of each finished trade grouped by pair
|
| `/performance` | | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | | Show account balance per currency
|
| `/balance` | | Show account balance per currency
|
||||||
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
||||||
|
@ -30,16 +31,20 @@ official commands. You can ask at any moment for help with `/help`.
|
||||||
| `/version` | | Show version
|
| `/version` | | Show version
|
||||||
|
|
||||||
## Telegram commands in action
|
## Telegram commands in action
|
||||||
|
|
||||||
Below, example of Telegram message you will receive for each command.
|
Below, example of Telegram message you will receive for each command.
|
||||||
|
|
||||||
### /start
|
### /start
|
||||||
|
|
||||||
> **Status:** `running`
|
> **Status:** `running`
|
||||||
|
|
||||||
### /stop
|
### /stop
|
||||||
|
|
||||||
> `Stopping trader ...`
|
> `Stopping trader ...`
|
||||||
> **Status:** `stopped`
|
> **Status:** `stopped`
|
||||||
|
|
||||||
## /status
|
## /status
|
||||||
|
|
||||||
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`
|
||||||
|
@ -54,6 +59,7 @@ For each open trade, the bot will send you the following message.
|
||||||
> **Open Order:** `None`
|
> **Open Order:** `None`
|
||||||
|
|
||||||
## /status table
|
## /status table
|
||||||
|
|
||||||
Return the status of all open trades in a table format.
|
Return the status of all open trades in a table format.
|
||||||
```
|
```
|
||||||
ID Pair Since Profit
|
ID Pair Since Profit
|
||||||
|
@ -63,6 +69,7 @@ Return the status of all open trades in a table format.
|
||||||
```
|
```
|
||||||
|
|
||||||
## /count
|
## /count
|
||||||
|
|
||||||
Return the number of trades used and available.
|
Return the number of trades used and available.
|
||||||
```
|
```
|
||||||
current max
|
current max
|
||||||
|
@ -71,6 +78,7 @@ current max
|
||||||
```
|
```
|
||||||
|
|
||||||
## /profit
|
## /profit
|
||||||
|
|
||||||
Return a summary of your profit/loss and performance.
|
Return a summary of your profit/loss and performance.
|
||||||
|
|
||||||
> **ROI:** Close trades
|
> **ROI:** Close trades
|
||||||
|
@ -90,7 +98,14 @@ Return a summary of your profit/loss and performance.
|
||||||
|
|
||||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||||
|
|
||||||
|
## /forcebuy <pair>
|
||||||
|
|
||||||
|
> **BITTREX**: Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
|
||||||
|
|
||||||
|
Note that for this to work, `forcebuy_enable` needs to be set to true.
|
||||||
|
|
||||||
## /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. `RCN/BTC 57.77%`
|
> 1. `RCN/BTC 57.77%`
|
||||||
|
@ -101,6 +116,7 @@ Return the performance of each crypto-currency the bot has sold.
|
||||||
> ...
|
> ...
|
||||||
|
|
||||||
## /balance
|
## /balance
|
||||||
|
|
||||||
Return the balance of all crypto-currency your have on the exchange.
|
Return the balance of all crypto-currency your have on the exchange.
|
||||||
|
|
||||||
> **Currency:** BTC
|
> **Currency:** BTC
|
||||||
|
@ -114,6 +130,7 @@ Return the balance of all crypto-currency your have on the exchange.
|
||||||
> **Pending:** 0.0
|
> **Pending:** 0.0
|
||||||
|
|
||||||
## /daily <n>
|
## /daily <n>
|
||||||
|
|
||||||
Per default `/daily` will return the 7 last days.
|
Per default `/daily` will return the 7 last days.
|
||||||
The example below if for `/daily 3`:
|
The example below if for `/daily 3`:
|
||||||
|
|
||||||
|
@ -127,11 +144,6 @@ Day Profit BTC Profit USD
|
||||||
```
|
```
|
||||||
|
|
||||||
## /version
|
## /version
|
||||||
|
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
### using proxy with telegram
|
|
||||||
```
|
|
||||||
$ export HTTP_PROXY="http://addr:port"
|
|
||||||
$ export HTTPS_PROXY="http://addr:port"
|
|
||||||
$ freqtrade
|
|
||||||
```
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ Possible parameters are:
|
||||||
* profit_fiat
|
* profit_fiat
|
||||||
* stake_currency
|
* stake_currency
|
||||||
* fiat_currency
|
* fiat_currency
|
||||||
|
* sell_reason
|
||||||
|
|
||||||
### Webhookstatus
|
### Webhookstatus
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.17.1'
|
__version__ = '0.18.0'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(BaseException):
|
class DependencyException(BaseException):
|
||||||
|
|
|
@ -104,10 +104,19 @@ class Arguments(object):
|
||||||
type=str,
|
type=str,
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--customhyperopt',
|
||||||
|
help='specify hyperopt class name (default: %(default)s)',
|
||||||
|
dest='hyperopt',
|
||||||
|
default=constants.DEFAULT_HYPEROPT,
|
||||||
|
type=str,
|
||||||
|
metavar='NAME',
|
||||||
|
)
|
||||||
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: %(const)s)',
|
' based on 24h BaseVolume (default: %(const)s)'
|
||||||
|
' DEPRECATED.',
|
||||||
dest='dynamic_whitelist',
|
dest='dynamic_whitelist',
|
||||||
const=constants.DYNAMIC_WHITELIST,
|
const=constants.DYNAMIC_WHITELIST,
|
||||||
type=int,
|
type=int,
|
||||||
|
@ -128,6 +137,22 @@ class Arguments(object):
|
||||||
"""
|
"""
|
||||||
Parses given arguments for Backtesting scripts.
|
Parses given arguments for Backtesting scripts.
|
||||||
"""
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'--eps', '--enable-position-stacking',
|
||||||
|
help='Allow buying the same pair multiple times (position stacking)',
|
||||||
|
action='store_true',
|
||||||
|
dest='position_stacking',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--dmmp', '--disable-max-market-positions',
|
||||||
|
help='Disable applying `max_open_trades` during backtest '
|
||||||
|
'(same as setting `max_open_trades` to a very high number)',
|
||||||
|
action='store_false',
|
||||||
|
dest='use_max_market_positions',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-l', '--live',
|
'-l', '--live',
|
||||||
help='using live data',
|
help='using live data',
|
||||||
|
@ -171,6 +196,27 @@ class Arguments(object):
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def edge_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Backtesting scripts.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--refresh-pairs-cached',
|
||||||
|
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||||
|
'exchange. Use it if you want to run your edge with up-to-date data.',
|
||||||
|
action='store_true',
|
||||||
|
dest='refresh_pairs',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--stoplosses',
|
||||||
|
help='defines a range of stoploss against which edge will assess the strategy '
|
||||||
|
'the format is "min,max,step" (without any space).'
|
||||||
|
'example: --stoplosses=-0.01,-0.1,-0.001',
|
||||||
|
type=str,
|
||||||
|
dest='stoploss_range',
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -184,6 +230,20 @@ class Arguments(object):
|
||||||
dest='ticker_interval',
|
dest='ticker_interval',
|
||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--timerange',
|
||||||
|
help='specify what timerange of data to use.',
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
dest='timerange',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Hyperopt scripts.
|
||||||
|
"""
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--eps', '--enable-position-stacking',
|
'--eps', '--enable-position-stacking',
|
||||||
help='Allow buying the same pair multiple times (position stacking)',
|
help='Allow buying the same pair multiple times (position stacking)',
|
||||||
|
@ -200,20 +260,6 @@ class Arguments(object):
|
||||||
dest='use_max_market_positions',
|
dest='use_max_market_positions',
|
||||||
default=True
|
default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
'--timerange',
|
|
||||||
help='specify what timerange of data to use.',
|
|
||||||
default=None,
|
|
||||||
type=str,
|
|
||||||
dest='timerange',
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
|
||||||
Parses given arguments for Hyperopt scripts.
|
|
||||||
"""
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-e', '--epochs',
|
'-e', '--epochs',
|
||||||
help='specify number of epochs (default: %(default)d)',
|
help='specify number of epochs (default: %(default)d)',
|
||||||
|
@ -237,7 +283,7 @@ class Arguments(object):
|
||||||
Builds and attaches all subcommands
|
Builds and attaches all subcommands
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
from freqtrade.optimize import backtesting, hyperopt
|
from freqtrade.optimize import backtesting, hyperopt, edge_cli
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
|
@ -247,6 +293,12 @@ class Arguments(object):
|
||||||
self.optimizer_shared_options(backtesting_cmd)
|
self.optimizer_shared_options(backtesting_cmd)
|
||||||
self.backtesting_options(backtesting_cmd)
|
self.backtesting_options(backtesting_cmd)
|
||||||
|
|
||||||
|
# Add edge subcommand
|
||||||
|
edge_cmd = subparsers.add_parser('edge', help='edge module')
|
||||||
|
edge_cmd.set_defaults(func=edge_cli.start)
|
||||||
|
self.optimizer_shared_options(edge_cmd)
|
||||||
|
self.edge_options(edge_cmd)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||||
|
@ -326,6 +378,15 @@ class Arguments(object):
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-c', '--config',
|
||||||
|
help='specify configuration file, used for additional exchange parameters',
|
||||||
|
dest='config',
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--days',
|
'--days',
|
||||||
help='Download data for number of days',
|
help='Download data for number of days',
|
||||||
|
@ -337,7 +398,7 @@ class Arguments(object):
|
||||||
|
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--exchange',
|
'--exchange',
|
||||||
help='Exchange name (default: %(default)s)',
|
help='Exchange name (default: %(default)s). Only valid if no config is provided',
|
||||||
dest='exchange',
|
dest='exchange',
|
||||||
type=str,
|
type=str,
|
||||||
default='bittrex'
|
default='bittrex'
|
||||||
|
|
|
@ -33,6 +33,7 @@ class Configuration(object):
|
||||||
Class to read and init the bot configuration
|
Class to read and init the bot configuration
|
||||||
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Namespace) -> None:
|
def __init__(self, args: Namespace) -> None:
|
||||||
self.args = args
|
self.args = args
|
||||||
self.config: Optional[Dict[str, Any]] = None
|
self.config: Optional[Dict[str, Any]] = None
|
||||||
|
@ -52,12 +53,18 @@ class Configuration(object):
|
||||||
if self.args.strategy_path:
|
if self.args.strategy_path:
|
||||||
config.update({'strategy_path': self.args.strategy_path})
|
config.update({'strategy_path': self.args.strategy_path})
|
||||||
|
|
||||||
|
# Add the hyperopt file to use
|
||||||
|
config.update({'hyperopt': self.args.hyperopt})
|
||||||
|
|
||||||
# Load Common configuration
|
# Load Common configuration
|
||||||
config = self._load_common_config(config)
|
config = self._load_common_config(config)
|
||||||
|
|
||||||
# Load Backtesting
|
# Load Backtesting
|
||||||
config = self._load_backtesting_config(config)
|
config = self._load_backtesting_config(config)
|
||||||
|
|
||||||
|
# Load Edge
|
||||||
|
config = self._load_edge_config(config)
|
||||||
|
|
||||||
# Load Hyperopt
|
# Load Hyperopt
|
||||||
config = self._load_hyperopt_config(config)
|
config = self._load_hyperopt_config(config)
|
||||||
|
|
||||||
|
@ -103,10 +110,14 @@ class Configuration(object):
|
||||||
|
|
||||||
# Add dynamic_whitelist if found
|
# Add dynamic_whitelist if found
|
||||||
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||||
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
|
# Update to volumePairList (the previous default)
|
||||||
logger.info(
|
config['pairlist'] = {'method': 'VolumePairList',
|
||||||
'Parameter --dynamic-whitelist detected. '
|
'config': {'number_assets': self.args.dynamic_whitelist}
|
||||||
'Using dynamically generated whitelist. '
|
}
|
||||||
|
logger.warning(
|
||||||
|
'Parameter --dynamic-whitelist has been deprecated, '
|
||||||
|
'and will be completely replaced by the whitelist dict in the future. '
|
||||||
|
'For now: using dynamically generated whitelist based on VolumePairList. '
|
||||||
'(not applicable with Backtesting and Hyperopt)'
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -127,6 +138,13 @@ class Configuration(object):
|
||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
logger.info('Dry run is disabled')
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
|
if config.get('forcebuy_enable', False):
|
||||||
|
logger.warning('`forcebuy` RPC message enabled.')
|
||||||
|
|
||||||
|
# Setting max_open_trades to infinite if -1
|
||||||
|
if config.get('max_open_trades') == -1:
|
||||||
|
config['max_open_trades'] = float('inf')
|
||||||
|
|
||||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
|
@ -210,6 +228,32 @@ class Configuration(object):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
def _load_edge_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Edge configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If --timerange is used we add it to the configuration
|
||||||
|
if 'timerange' in self.args and self.args.timerange:
|
||||||
|
config.update({'timerange': self.args.timerange})
|
||||||
|
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
||||||
|
|
||||||
|
# If --timerange is used we add it to the configuration
|
||||||
|
if 'stoploss_range' in self.args and self.args.stoploss_range:
|
||||||
|
txt_range = eval(self.args.stoploss_range)
|
||||||
|
config['edge'].update({'stoploss_range_min': txt_range[0]})
|
||||||
|
config['edge'].update({'stoploss_range_max': txt_range[1]})
|
||||||
|
config['edge'].update({'stoploss_range_step': txt_range[2]})
|
||||||
|
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
|
||||||
|
|
||||||
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
||||||
|
config.update({'refresh_pairs': True})
|
||||||
|
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
||||||
|
|
||||||
|
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]:
|
||||||
"""
|
"""
|
||||||
Extract information for sys.argv and load Hyperopt configuration
|
Extract information for sys.argv and load Hyperopt configuration
|
||||||
|
@ -235,7 +279,7 @@ class Configuration(object):
|
||||||
:return: Returns the config if valid, otherwise throw an exception
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
validate(conf, constants.CONF_SCHEMA)
|
validate(conf, constants.CONF_SCHEMA, Draft4Validator)
|
||||||
return conf
|
return conf
|
||||||
except ValidationError as exception:
|
except ValidationError as exception:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
|
@ -271,6 +315,11 @@ class Configuration(object):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
exception_msg
|
exception_msg
|
||||||
)
|
)
|
||||||
|
# Depreciation warning
|
||||||
|
if 'ccxt_rate_limit' in config.get('exchange', {}):
|
||||||
|
logger.warning("`ccxt_rate_limit` has been deprecated in favor of "
|
||||||
|
"`ccxt_config` and `ccxt_async_config` and will be removed "
|
||||||
|
"in a future version.")
|
||||||
|
|
||||||
logger.debug('Exchange "%s" supported', exchange)
|
logger.debug('Exchange "%s" supported', exchange)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -9,10 +9,15 @@ 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_HYPEROPT = 'DefaultHyperOpts'
|
||||||
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
|
||||||
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
|
||||||
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
UNLIMITED_STAKE_AMOUNT = 'unlimited'
|
||||||
|
REQUIRED_ORDERTIF = ['buy', 'sell']
|
||||||
|
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
|
||||||
|
|
||||||
TICKER_INTERVAL_MINUTES = {
|
TICKER_INTERVAL_MINUTES = {
|
||||||
'1m': 1,
|
'1m': 1,
|
||||||
|
@ -43,7 +48,7 @@ SUPPORTED_FIAT = [
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
'max_open_trades': {'type': 'integer', 'minimum': -1},
|
||||||
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||||
'stake_amount': {
|
'stake_amount': {
|
||||||
|
@ -101,7 +106,26 @@ CONF_SCHEMA = {
|
||||||
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'order_types': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
|
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
|
'stoploss_on_exchange': {'type': 'boolean'}
|
||||||
|
},
|
||||||
|
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
|
},
|
||||||
|
'order_time_in_force': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
|
||||||
|
'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
|
||||||
|
},
|
||||||
|
'required': ['buy', 'sell']
|
||||||
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
|
'edge': {'$ref': '#/definitions/edge'},
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -110,6 +134,14 @@ CONF_SCHEMA = {
|
||||||
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
|
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'pairlist': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
|
||||||
|
'config': {'type': 'object'}
|
||||||
|
},
|
||||||
|
'required': ['method']
|
||||||
|
},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -130,6 +162,7 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'db_url': {'type': 'string'},
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
|
'forcebuy_enable': {'type': 'boolean'},
|
||||||
'internals': {
|
'internals': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -164,9 +197,30 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
},
|
},
|
||||||
'outdated_offset': {'type': 'integer', 'minimum': 1}
|
'outdated_offset': {'type': 'integer', 'minimum': 1},
|
||||||
|
'ccxt_config': {'type': 'object'},
|
||||||
|
'ccxt_async_config': {'type': 'object'}
|
||||||
},
|
},
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
|
},
|
||||||
|
'edge': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
"enabled": {'type': 'boolean'},
|
||||||
|
"process_throttle_secs": {'type': 'integer', 'minimum': 600},
|
||||||
|
"calculate_since_number_of_days": {'type': 'integer'},
|
||||||
|
"allowed_risk": {'type': 'number'},
|
||||||
|
"capital_available_percentage": {'type': 'number'},
|
||||||
|
"stoploss_range_min": {'type': 'number'},
|
||||||
|
"stoploss_range_max": {'type': 'number'},
|
||||||
|
"stoploss_range_step": {'type': 'number'},
|
||||||
|
"minimum_winrate": {'type': 'number'},
|
||||||
|
"minimum_expectancy": {'type': 'number'},
|
||||||
|
"min_trade_number": {'type': 'number'},
|
||||||
|
"max_trade_duration_minute": {'type': 'integer'},
|
||||||
|
"remove_pumps": {'type': 'boolean'}
|
||||||
|
},
|
||||||
|
'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'anyOf': [
|
'anyOf': [
|
||||||
|
|
8
freqtrade/data/__init__.py
Normal file
8
freqtrade/data/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
"""
|
||||||
|
Module to handle data operations for freqtrade
|
||||||
|
"""
|
||||||
|
|
||||||
|
# limit what's imported when using `from freqtrad.data import *``
|
||||||
|
__all__ = [
|
||||||
|
'converter'
|
||||||
|
]
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
Functions to convert data from one format to another
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
@ -10,10 +10,11 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given ticker history
|
Converts a ticker-list (format ccxt.fetch_ohlcv) to a Dataframe
|
||||||
:param ticker: See exchange.get_candle_history
|
:param ticker: ticker list, as returned by exchange.async_get_candle_history
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
|
logger.debug("Parsing tickerlist to dataframe")
|
||||||
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
frame = DataFrame(ticker, columns=cols)
|
frame = DataFrame(ticker, columns=cols)
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
'volume': 'max',
|
'volume': 'max',
|
||||||
})
|
})
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
|
logger.debug('Dropping last candle')
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
251
freqtrade/data/history.py
Normal file
251
freqtrade/data/history.py
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
"""
|
||||||
|
Handle historic data (ohlcv).
|
||||||
|
includes:
|
||||||
|
* load data for a pair (or a list of pairs) from disk
|
||||||
|
* download data from exchange and store to disk
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gzip
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
import ujson
|
||||||
|
|
||||||
|
from freqtrade import misc, constants, OperationalException
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def json_load(data):
|
||||||
|
"""
|
||||||
|
load data with ujson
|
||||||
|
Use this to have a consistent experience,
|
||||||
|
otherwise "precise_float" needs to be passed to all load operations
|
||||||
|
"""
|
||||||
|
return ujson.load(data, precise_float=True)
|
||||||
|
|
||||||
|
|
||||||
|
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Trim tickerlist based on given timerange
|
||||||
|
"""
|
||||||
|
if not tickerlist:
|
||||||
|
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(
|
||||||
|
datadir: Optional[Path], pair: str,
|
||||||
|
ticker_interval: str,
|
||||||
|
timerange: Optional[TimeRange] = None) -> Optional[list]:
|
||||||
|
"""
|
||||||
|
Load a pair from file, either .json.gz or .json
|
||||||
|
:return tickerlist or None if unsuccesful
|
||||||
|
"""
|
||||||
|
path = make_testdata_path(datadir)
|
||||||
|
pair_s = pair.replace('/', '_')
|
||||||
|
file = path.joinpath(f'{pair_s}-{ticker_interval}.json')
|
||||||
|
gzipfile = file.with_suffix(file.suffix + '.gz')
|
||||||
|
|
||||||
|
# Try gzip file first, otherwise regular json file.
|
||||||
|
if gzipfile.is_file():
|
||||||
|
logger.debug('Loading ticker data from file %s', gzipfile)
|
||||||
|
with gzip.open(gzipfile) as tickerdata:
|
||||||
|
pairdata = json_load(tickerdata)
|
||||||
|
elif file.is_file():
|
||||||
|
logger.debug('Loading ticker data from file %s', file)
|
||||||
|
with open(file) as tickerdata:
|
||||||
|
pairdata = json_load(tickerdata)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if timerange:
|
||||||
|
pairdata = trim_tickerlist(pairdata, timerange)
|
||||||
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
def load_pair_history(pair: str,
|
||||||
|
ticker_interval: str,
|
||||||
|
datadir: Optional[Path],
|
||||||
|
timerange: TimeRange = TimeRange(None, None, 0, 0),
|
||||||
|
refresh_pairs: bool = False,
|
||||||
|
exchange: Optional[Exchange] = None,
|
||||||
|
) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Loads cached ticker history for the given pair.
|
||||||
|
:return: DataFrame with ohlcv data
|
||||||
|
"""
|
||||||
|
|
||||||
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
|
# If the user force the refresh of pairs
|
||||||
|
if refresh_pairs:
|
||||||
|
if not exchange:
|
||||||
|
raise OperationalException("Exchange needs to be initialized when "
|
||||||
|
"calling load_data with refresh_pairs=True")
|
||||||
|
|
||||||
|
logger.info('Download data for all pairs and store them in %s', datadir)
|
||||||
|
download_pair_history(datadir=datadir,
|
||||||
|
exchange=exchange,
|
||||||
|
pair=pair,
|
||||||
|
tick_interval=ticker_interval,
|
||||||
|
timerange=timerange)
|
||||||
|
|
||||||
|
if pairdata:
|
||||||
|
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||||
|
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||||
|
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
|
if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000:
|
||||||
|
logger.warning('Missing data at end for pair %s, data ends at %s',
|
||||||
|
pair,
|
||||||
|
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
|
return parse_ticker_dataframe(pairdata)
|
||||||
|
else:
|
||||||
|
logger.warning('No data for pair: "%s", Interval: %s. '
|
||||||
|
'Use --refresh-pairs-cached to download the data',
|
||||||
|
pair, ticker_interval)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(datadir: Optional[Path],
|
||||||
|
ticker_interval: str,
|
||||||
|
pairs: List[str],
|
||||||
|
refresh_pairs: bool = False,
|
||||||
|
exchange: Optional[Exchange] = None,
|
||||||
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, DataFrame]:
|
||||||
|
"""
|
||||||
|
Loads ticker history data for a list of pairs the given parameters
|
||||||
|
:return: dict(<pair>:<tickerlist>)
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||||
|
datadir=datadir, timerange=timerange,
|
||||||
|
refresh_pairs=refresh_pairs,
|
||||||
|
exchange=exchange)
|
||||||
|
if hist is not None:
|
||||||
|
result[pair] = hist
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def make_testdata_path(datadir: Optional[Path]) -> Path:
|
||||||
|
"""Return the path where testdata files are stored"""
|
||||||
|
return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def load_cached_data_for_updating(filename: Path, tick_interval: str,
|
||||||
|
timerange: Optional[TimeRange]) -> Tuple[List[Any],
|
||||||
|
Optional[int]]:
|
||||||
|
"""
|
||||||
|
Load cached data and choose what part of the data should be updated
|
||||||
|
"""
|
||||||
|
|
||||||
|
since_ms = None
|
||||||
|
|
||||||
|
# user sets timerange, so find the start time
|
||||||
|
if timerange:
|
||||||
|
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 filename.is_file():
|
||||||
|
with open(filename, "rt") as file:
|
||||||
|
data = json_load(file)
|
||||||
|
# remove the last item, could be incomplete candle
|
||||||
|
if data:
|
||||||
|
data.pop()
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
|
||||||
|
if data:
|
||||||
|
if since_ms and since_ms < data[0][0]:
|
||||||
|
# Earlier data than existing data requested, redownload all
|
||||||
|
data = []
|
||||||
|
else:
|
||||||
|
# a part of the data was already downloaded, so download unexist data only
|
||||||
|
since_ms = data[-1][0] + 1
|
||||||
|
|
||||||
|
return (data, since_ms)
|
||||||
|
|
||||||
|
|
||||||
|
def download_pair_history(datadir: Optional[Path],
|
||||||
|
exchange: Exchange,
|
||||||
|
pair: str,
|
||||||
|
tick_interval: str = '5m',
|
||||||
|
timerange: Optional[TimeRange] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Download the latest ticker intervals from the exchange for the pair passed in parameters
|
||||||
|
The data is downloaded starting from the last correct ticker interval data that
|
||||||
|
exists 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 pair: pair to download
|
||||||
|
:param tick_interval: ticker interval
|
||||||
|
:param timerange: range of time to download
|
||||||
|
:return: bool with success state
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path = make_testdata_path(datadir)
|
||||||
|
filepair = pair.replace("/", "_")
|
||||||
|
filename = path.joinpath(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')
|
||||||
|
|
||||||
|
# Default since_ms to 30 days if nothing is given
|
||||||
|
new_data = exchange.get_history(pair=pair, tick_interval=tick_interval,
|
||||||
|
since_ms=since_ms if since_ms
|
||||||
|
else
|
||||||
|
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
||||||
|
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]))
|
||||||
|
|
||||||
|
misc.file_dump_json(filename, data)
|
||||||
|
return True
|
||||||
|
except BaseException:
|
||||||
|
logger.info('Failed to download the pair: "%s", Interval: %s',
|
||||||
|
pair, tick_interval)
|
||||||
|
return False
|
434
freqtrade/edge/__init__.py
Normal file
434
freqtrade/edge/__init__.py
Normal file
|
@ -0,0 +1,434 @@
|
||||||
|
# pragma pylint: disable=W0603
|
||||||
|
""" Edge positioning package """
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, NamedTuple
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import numpy as np
|
||||||
|
import utils_find_1st as utf1st
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import constants, OperationalException
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PairInfo(NamedTuple):
|
||||||
|
stoploss: float
|
||||||
|
winrate: float
|
||||||
|
risk_reward_ratio: float
|
||||||
|
required_risk_reward: float
|
||||||
|
expectancy: float
|
||||||
|
nb_trades: int
|
||||||
|
avg_trade_duration: float
|
||||||
|
|
||||||
|
|
||||||
|
class Edge():
|
||||||
|
"""
|
||||||
|
Calculates Win Rate, Risk Reward Ratio, Expectancy
|
||||||
|
against historical data for a give set of markets and a strategy
|
||||||
|
it then adjusts stoploss and position size accordingly
|
||||||
|
and force it into the strategy
|
||||||
|
Author: https://github.com/mishaker
|
||||||
|
"""
|
||||||
|
|
||||||
|
config: Dict = {}
|
||||||
|
_cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], exchange, strategy) -> None:
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.exchange = exchange
|
||||||
|
self.strategy = strategy
|
||||||
|
self.ticker_interval = self.strategy.ticker_interval
|
||||||
|
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
|
||||||
|
self.get_timeframe = get_timeframe
|
||||||
|
self.advise_sell = self.strategy.advise_sell
|
||||||
|
self.advise_buy = self.strategy.advise_buy
|
||||||
|
|
||||||
|
self.edge_config = self.config.get('edge', {})
|
||||||
|
self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs
|
||||||
|
self._final_pairs: list = []
|
||||||
|
|
||||||
|
# checking max_open_trades. it should be -1 as with Edge
|
||||||
|
# the number of trades is determined by position size
|
||||||
|
if self.config['max_open_trades'] != -1:
|
||||||
|
logger.critical('max_open_trades should be -1 in config !')
|
||||||
|
|
||||||
|
if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
|
raise OperationalException('Edge works only with unlimited stake amount')
|
||||||
|
|
||||||
|
self._capital_percentage: float = self.edge_config.get('capital_available_percentage')
|
||||||
|
self._allowed_risk: float = self.edge_config.get('allowed_risk')
|
||||||
|
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
|
||||||
|
self._last_updated: int = 0 # Timestamp of pairs last updated time
|
||||||
|
self._refresh_pairs = True
|
||||||
|
|
||||||
|
self._stoploss_range_min = float(self.edge_config.get('stoploss_range_min', -0.01))
|
||||||
|
self._stoploss_range_max = float(self.edge_config.get('stoploss_range_max', -0.05))
|
||||||
|
self._stoploss_range_step = float(self.edge_config.get('stoploss_range_step', -0.001))
|
||||||
|
|
||||||
|
# calculating stoploss range
|
||||||
|
self._stoploss_range = np.arange(
|
||||||
|
self._stoploss_range_min,
|
||||||
|
self._stoploss_range_max,
|
||||||
|
self._stoploss_range_step
|
||||||
|
)
|
||||||
|
|
||||||
|
self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift(
|
||||||
|
days=-1 * self._since_number_of_days).format('YYYYMMDD'))
|
||||||
|
|
||||||
|
self.fee = self.exchange.get_fee()
|
||||||
|
|
||||||
|
def calculate(self) -> bool:
|
||||||
|
pairs = self.config['exchange']['pair_whitelist']
|
||||||
|
heartbeat = self.edge_config.get('process_throttle_secs')
|
||||||
|
|
||||||
|
if (self._last_updated > 0) and (
|
||||||
|
self._last_updated + heartbeat > arrow.utcnow().timestamp):
|
||||||
|
return False
|
||||||
|
|
||||||
|
data: Dict[str, Any] = {}
|
||||||
|
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||||
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
|
data = history.load_data(
|
||||||
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
|
pairs=pairs,
|
||||||
|
ticker_interval=self.ticker_interval,
|
||||||
|
refresh_pairs=self._refresh_pairs,
|
||||||
|
exchange=self.exchange,
|
||||||
|
timerange=self._timerange
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
# Reinitializing cached pairs
|
||||||
|
self._cached_pairs = {}
|
||||||
|
logger.critical("No data found. Edge is stopped ...")
|
||||||
|
return False
|
||||||
|
|
||||||
|
preprocessed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
|
# Print timeframe
|
||||||
|
min_date, max_date = self.get_timeframe(preprocessed)
|
||||||
|
logger.info(
|
||||||
|
'Measuring data from %s up to %s (%s days) ...',
|
||||||
|
min_date.isoformat(),
|
||||||
|
max_date.isoformat(),
|
||||||
|
(max_date - min_date).days
|
||||||
|
)
|
||||||
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
|
||||||
|
|
||||||
|
trades: list = []
|
||||||
|
for pair, pair_data in preprocessed.items():
|
||||||
|
# Sorting dataframe by date and reset index
|
||||||
|
pair_data = pair_data.sort_values(by=['date'])
|
||||||
|
pair_data = pair_data.reset_index(drop=True)
|
||||||
|
|
||||||
|
ticker_data = self.advise_sell(
|
||||||
|
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||||
|
|
||||||
|
trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range)
|
||||||
|
|
||||||
|
# If no trade found then exit
|
||||||
|
if len(trades) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Fill missing, calculable columns, profit, duration , abs etc.
|
||||||
|
trades_df = self._fill_calculable_fields(DataFrame(trades))
|
||||||
|
self._cached_pairs = self._process_expectancy(trades_df)
|
||||||
|
self._last_updated = arrow.utcnow().timestamp
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def stake_amount(self, pair: str, free_capital: float,
|
||||||
|
total_capital: float, capital_in_trade: float) -> float:
|
||||||
|
stoploss = self.stoploss(pair)
|
||||||
|
available_capital = (total_capital + capital_in_trade) * self._capital_percentage
|
||||||
|
allowed_capital_at_risk = available_capital * self._allowed_risk
|
||||||
|
max_position_size = abs(allowed_capital_at_risk / stoploss)
|
||||||
|
position_size = min(max_position_size, free_capital)
|
||||||
|
if pair in self._cached_pairs:
|
||||||
|
logger.info(
|
||||||
|
'winrate: %s, expectancy: %s, position size: %s, pair: %s,'
|
||||||
|
' capital in trade: %s, free capital: %s, total capital: %s,'
|
||||||
|
' stoploss: %s, available capital: %s.',
|
||||||
|
self._cached_pairs[pair].winrate,
|
||||||
|
self._cached_pairs[pair].expectancy,
|
||||||
|
position_size, pair,
|
||||||
|
capital_in_trade, free_capital, total_capital,
|
||||||
|
stoploss, available_capital
|
||||||
|
)
|
||||||
|
return round(position_size, 15)
|
||||||
|
|
||||||
|
def stoploss(self, pair: str) -> float:
|
||||||
|
if pair in self._cached_pairs:
|
||||||
|
return self._cached_pairs[pair].stoploss
|
||||||
|
else:
|
||||||
|
logger.warning('tried to access stoploss of a non-existing pair, '
|
||||||
|
'strategy stoploss is returned instead.')
|
||||||
|
return self.strategy.stoploss
|
||||||
|
|
||||||
|
def adjust(self, pairs) -> list:
|
||||||
|
"""
|
||||||
|
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||||
|
"""
|
||||||
|
final = []
|
||||||
|
for pair, info in self._cached_pairs.items():
|
||||||
|
if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \
|
||||||
|
info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \
|
||||||
|
pair in pairs:
|
||||||
|
final.append(pair)
|
||||||
|
|
||||||
|
if self._final_pairs != final:
|
||||||
|
self._final_pairs = final
|
||||||
|
if self._final_pairs:
|
||||||
|
logger.info('Edge validated only %s', self._final_pairs)
|
||||||
|
else:
|
||||||
|
logger.info('Edge removed all pairs as no pair with minimum expectancy was found !')
|
||||||
|
|
||||||
|
return self._final_pairs
|
||||||
|
|
||||||
|
def _fill_calculable_fields(self, result: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
The result frame contains a number of columns that are calculable
|
||||||
|
from other columns. These are left blank till all rows are added,
|
||||||
|
to be populated in single vector calls.
|
||||||
|
|
||||||
|
Columns to be populated are:
|
||||||
|
- Profit
|
||||||
|
- trade duration
|
||||||
|
- profit abs
|
||||||
|
:param result Dataframe
|
||||||
|
:return: result Dataframe
|
||||||
|
"""
|
||||||
|
|
||||||
|
# stake and fees
|
||||||
|
# stake = 0.015
|
||||||
|
# 0.05% is 0.0005
|
||||||
|
# fee = 0.001
|
||||||
|
|
||||||
|
# we set stake amount to an arbitrary amount.
|
||||||
|
# as it doesn't change the calculation.
|
||||||
|
# all returned values are relative. they are percentages.
|
||||||
|
stake = 0.015
|
||||||
|
fee = self.fee
|
||||||
|
open_fee = fee / 2
|
||||||
|
close_fee = fee / 2
|
||||||
|
|
||||||
|
result['trade_duration'] = result['close_time'] - result['open_time']
|
||||||
|
|
||||||
|
result['trade_duration'] = result['trade_duration'].map(
|
||||||
|
lambda x: int(x.total_seconds() / 60))
|
||||||
|
|
||||||
|
# Spends, Takes, Profit, Absolute Profit
|
||||||
|
|
||||||
|
# Buy Price
|
||||||
|
result['buy_vol'] = stake / result['open_rate'] # How many target are we buying
|
||||||
|
result['buy_fee'] = stake * open_fee
|
||||||
|
result['buy_spend'] = stake + result['buy_fee'] # How much we're spending
|
||||||
|
|
||||||
|
# Sell price
|
||||||
|
result['sell_sum'] = result['buy_vol'] * result['close_rate']
|
||||||
|
result['sell_fee'] = result['sell_sum'] * close_fee
|
||||||
|
result['sell_take'] = result['sell_sum'] - result['sell_fee']
|
||||||
|
|
||||||
|
# profit_percent
|
||||||
|
result['profit_percent'] = (result['sell_take'] - result['buy_spend']) / result['buy_spend']
|
||||||
|
|
||||||
|
# Absolute profit
|
||||||
|
result['profit_abs'] = result['sell_take'] - result['buy_spend']
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs
|
||||||
|
The calulation will be done per pair and per strategy.
|
||||||
|
"""
|
||||||
|
# Removing pairs having less than min_trades_number
|
||||||
|
min_trades_number = self.edge_config.get('min_trade_number', 10)
|
||||||
|
results = results.groupby(['pair', 'stoploss']).filter(lambda x: len(x) > min_trades_number)
|
||||||
|
###################################
|
||||||
|
|
||||||
|
# Removing outliers (Only Pumps) from the dataset
|
||||||
|
# The method to detect outliers is to calculate standard deviation
|
||||||
|
# Then every value more than (standard deviation + 2*average) is out (pump)
|
||||||
|
#
|
||||||
|
# Removing Pumps
|
||||||
|
if self.edge_config.get('remove_pumps', False):
|
||||||
|
results = results.groupby(['pair', 'stoploss']).apply(
|
||||||
|
lambda x: x[x['profit_abs'] < 2 * x['profit_abs'].std() + x['profit_abs'].mean()])
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
|
# Removing trades having a duration more than X minutes (set in config)
|
||||||
|
max_trade_duration = self.edge_config.get('max_trade_duration_minute', 1440)
|
||||||
|
results = results[results.trade_duration < max_trade_duration]
|
||||||
|
#######################################################################
|
||||||
|
|
||||||
|
if results.empty:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
groupby_aggregator = {
|
||||||
|
'profit_abs': [
|
||||||
|
('nb_trades', 'count'), # number of all trades
|
||||||
|
('profit_sum', lambda x: x[x > 0].sum()), # cumulative profit of all winning trades
|
||||||
|
('loss_sum', lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades
|
||||||
|
('nb_win_trades', lambda x: x[x > 0].count()) # number of winning trades
|
||||||
|
],
|
||||||
|
'trade_duration': [('avg_trade_duration', 'mean')]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Group by (pair and stoploss) by applying above aggregator
|
||||||
|
df = results.groupby(['pair', 'stoploss'])['profit_abs', 'trade_duration'].agg(
|
||||||
|
groupby_aggregator).reset_index(col_level=1)
|
||||||
|
|
||||||
|
# Dropping level 0 as we don't need it
|
||||||
|
df.columns = df.columns.droplevel(0)
|
||||||
|
|
||||||
|
# Calculating number of losing trades, average win and average loss
|
||||||
|
df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades']
|
||||||
|
df['average_win'] = df['profit_sum'] / df['nb_win_trades']
|
||||||
|
df['average_loss'] = df['loss_sum'] / df['nb_loss_trades']
|
||||||
|
|
||||||
|
# Win rate = number of profitable trades / number of trades
|
||||||
|
df['winrate'] = df['nb_win_trades'] / df['nb_trades']
|
||||||
|
|
||||||
|
# risk_reward_ratio = average win / average loss
|
||||||
|
df['risk_reward_ratio'] = df['average_win'] / df['average_loss']
|
||||||
|
|
||||||
|
# required_risk_reward = (1 / winrate) - 1
|
||||||
|
df['required_risk_reward'] = (1 / df['winrate']) - 1
|
||||||
|
|
||||||
|
# expectancy = (risk_reward_ratio * winrate) - (lossrate)
|
||||||
|
df['expectancy'] = (df['risk_reward_ratio'] * df['winrate']) - (1 - df['winrate'])
|
||||||
|
|
||||||
|
# sort by expectancy and stoploss
|
||||||
|
df = df.sort_values(by=['expectancy', 'stoploss'], ascending=False).groupby(
|
||||||
|
'pair').first().sort_values(by=['expectancy'], ascending=False).reset_index()
|
||||||
|
|
||||||
|
final = {}
|
||||||
|
for x in df.itertuples():
|
||||||
|
final[x.pair] = PairInfo(
|
||||||
|
x.stoploss,
|
||||||
|
x.winrate,
|
||||||
|
x.risk_reward_ratio,
|
||||||
|
x.required_risk_reward,
|
||||||
|
x.expectancy,
|
||||||
|
x.nb_trades,
|
||||||
|
x.avg_trade_duration
|
||||||
|
)
|
||||||
|
|
||||||
|
# Returning a list of pairs in order of "expectancy"
|
||||||
|
return final
|
||||||
|
|
||||||
|
def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range):
|
||||||
|
buy_column = ticker_data['buy'].values
|
||||||
|
sell_column = ticker_data['sell'].values
|
||||||
|
date_column = ticker_data['date'].values
|
||||||
|
ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values
|
||||||
|
|
||||||
|
result: list = []
|
||||||
|
for stoploss in stoploss_range:
|
||||||
|
result += self._detect_next_stop_or_sell_point(
|
||||||
|
buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column,
|
||||||
|
ohlc_columns, stoploss, pair, start_point=0):
|
||||||
|
"""
|
||||||
|
Iterate through ohlc_columns recursively in order to find the next trade
|
||||||
|
Next trade opens from the first buy signal noticed to
|
||||||
|
The sell or stoploss signal after it.
|
||||||
|
It then calls itself cutting OHLC, buy_column, sell_colum and date_column
|
||||||
|
Cut from (the exit trade index) + 1
|
||||||
|
Author: https://github.com/mishaker
|
||||||
|
"""
|
||||||
|
|
||||||
|
result: list = []
|
||||||
|
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||||
|
|
||||||
|
# return empty if we don't find trade entry (i.e. buy==1) or
|
||||||
|
# we find a buy but at the of array
|
||||||
|
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
open_trade_index += 1 # when a buy signal is seen,
|
||||||
|
# trade opens in reality on the next candle
|
||||||
|
|
||||||
|
stop_price_percentage = stoploss + 1
|
||||||
|
open_price = ohlc_columns[open_trade_index, 0]
|
||||||
|
stop_price = (open_price * stop_price_percentage)
|
||||||
|
|
||||||
|
# Searching for the index where stoploss is hit
|
||||||
|
stop_index = utf1st.find_1st(
|
||||||
|
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller)
|
||||||
|
|
||||||
|
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||||
|
if stop_index == -1:
|
||||||
|
stop_index = float('inf')
|
||||||
|
|
||||||
|
# Searching for the index where sell is hit
|
||||||
|
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||||
|
|
||||||
|
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||||
|
if sell_index == -1:
|
||||||
|
sell_index = float('inf')
|
||||||
|
|
||||||
|
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||||
|
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||||
|
# And stop iterating there is no more entry
|
||||||
|
if stop_index == sell_index == float('inf'):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if stop_index <= sell_index:
|
||||||
|
exit_index = open_trade_index + stop_index
|
||||||
|
exit_type = SellType.STOP_LOSS
|
||||||
|
exit_price = stop_price
|
||||||
|
elif stop_index > sell_index:
|
||||||
|
# if exit is SELL then we exit at the next candle
|
||||||
|
exit_index = open_trade_index + sell_index + 1
|
||||||
|
|
||||||
|
# check if we have the next candle
|
||||||
|
if len(ohlc_columns) - 1 < exit_index:
|
||||||
|
return []
|
||||||
|
|
||||||
|
exit_type = SellType.SELL_SIGNAL
|
||||||
|
exit_price = ohlc_columns[exit_index, 0]
|
||||||
|
|
||||||
|
trade = {'pair': pair,
|
||||||
|
'stoploss': stoploss,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': date_column[open_trade_index],
|
||||||
|
'close_time': date_column[exit_index],
|
||||||
|
'open_index': start_point + open_trade_index,
|
||||||
|
'close_index': start_point + exit_index,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': round(open_price, 15),
|
||||||
|
'close_rate': round(exit_price, 15),
|
||||||
|
'exit_type': exit_type
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(trade)
|
||||||
|
|
||||||
|
# Calling again the same function recursively but giving
|
||||||
|
# it a view of exit_index till the end of array
|
||||||
|
return result + self._detect_next_stop_or_sell_point(
|
||||||
|
buy_column[exit_index:],
|
||||||
|
sell_column[exit_index:],
|
||||||
|
date_column[exit_index:],
|
||||||
|
ohlc_columns[exit_index:],
|
||||||
|
stoploss,
|
||||||
|
pair,
|
||||||
|
(start_point + exit_index)
|
||||||
|
)
|
|
@ -7,12 +7,14 @@ from typing import List, Dict, Tuple, Any, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import floor, ceil
|
from math import floor, ceil
|
||||||
|
|
||||||
|
import arrow
|
||||||
import asyncio
|
import asyncio
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.async_support as ccxt_async
|
import ccxt.async_support as ccxt_async
|
||||||
import arrow
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -64,14 +66,8 @@ def retrier(f):
|
||||||
|
|
||||||
class Exchange(object):
|
class Exchange(object):
|
||||||
|
|
||||||
# Current selected exchange
|
|
||||||
_api: ccxt.Exchange = None
|
|
||||||
_api_async: ccxt_async.Exchange = None
|
|
||||||
_conf: Dict = {}
|
_conf: Dict = {}
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
|
||||||
_dry_run_open_orders: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
|
@ -87,21 +83,27 @@ class Exchange(object):
|
||||||
self._pairs_last_refresh_time: Dict[str, int] = {}
|
self._pairs_last_refresh_time: Dict[str, int] = {}
|
||||||
|
|
||||||
# Holds candles
|
# Holds candles
|
||||||
self.klines: Dict[str, Any] = {}
|
self._klines: Dict[str, DataFrame] = {}
|
||||||
|
|
||||||
|
# Holds all open sell orders for dry_run
|
||||||
|
self._dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
self._api = self._init_ccxt(exchange_config)
|
self._api: ccxt.Exchange = self._init_ccxt(
|
||||||
self._api_async = self._init_ccxt(exchange_config, ccxt_async)
|
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
|
||||||
|
self._api_async: ccxt_async.Exchange = self._init_ccxt(
|
||||||
|
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
|
||||||
|
|
||||||
logger.info('Using Exchange "%s"', self.name)
|
logger.info('Using Exchange "%s"', self.name)
|
||||||
|
|
||||||
self.markets = self._load_markets()
|
self.markets = self._load_markets()
|
||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
self.validate_ordertypes(config.get('order_types', {}))
|
||||||
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
if config.get('ticker_interval'):
|
if config.get('ticker_interval'):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
self.validate_timeframes(config['ticker_interval'])
|
self.validate_timeframes(config['ticker_interval'])
|
||||||
|
@ -114,7 +116,8 @@ class Exchange(object):
|
||||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt) -> ccxt.Exchange:
|
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
|
||||||
|
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
ccxt instance.
|
ccxt instance.
|
||||||
|
@ -124,14 +127,20 @@ class Exchange(object):
|
||||||
|
|
||||||
if name not in ccxt_module.exchanges:
|
if name not in ccxt_module.exchanges:
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
try:
|
|
||||||
api = getattr(ccxt_module, name.lower())({
|
ex_config = {
|
||||||
'apiKey': exchange_config.get('key'),
|
'apiKey': exchange_config.get('key'),
|
||||||
'secret': exchange_config.get('secret'),
|
'secret': exchange_config.get('secret'),
|
||||||
'password': exchange_config.get('password'),
|
'password': exchange_config.get('password'),
|
||||||
'uid': exchange_config.get('uid', ''),
|
'uid': exchange_config.get('uid', ''),
|
||||||
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True)
|
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True)
|
||||||
})
|
}
|
||||||
|
if ccxt_kwargs:
|
||||||
|
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||||
|
ex_config.update(ccxt_kwargs)
|
||||||
|
try:
|
||||||
|
|
||||||
|
api = getattr(ccxt_module, name.lower())(ex_config)
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
|
||||||
|
@ -149,6 +158,12 @@ class Exchange(object):
|
||||||
"""exchange ccxt id"""
|
"""exchange ccxt id"""
|
||||||
return self._api.id
|
return self._api.id
|
||||||
|
|
||||||
|
def klines(self, pair: str, copy=True) -> DataFrame:
|
||||||
|
if pair in self._klines:
|
||||||
|
return self._klines[pair].copy() if copy else self._klines[pair]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||||
if exchange_config.get('sandbox'):
|
if exchange_config.get('sandbox'):
|
||||||
if api.urls.get('test'):
|
if api.urls.get('test'):
|
||||||
|
@ -199,7 +214,8 @@ class Exchange(object):
|
||||||
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||||
if self.markets and pair not in self.markets:
|
if self.markets and pair not in self.markets:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available at {self.name}')
|
f'Pair {pair} is not available at {self.name}'
|
||||||
|
f'Please remove {pair} from your whitelist.')
|
||||||
|
|
||||||
def validate_timeframes(self, timeframe: List[str]) -> None:
|
def validate_timeframes(self, timeframe: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -210,6 +226,30 @@ class Exchange(object):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
||||||
|
|
||||||
|
def validate_ordertypes(self, order_types: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Checks if order-types configured in strategy/config are supported
|
||||||
|
"""
|
||||||
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
|
if not self.exchange_has('createMarketOrder'):
|
||||||
|
raise OperationalException(
|
||||||
|
f'Exchange {self.name} does not support market orders.')
|
||||||
|
|
||||||
|
if order_types.get('stoploss_on_exchange'):
|
||||||
|
if self.name is not 'Binance':
|
||||||
|
raise OperationalException(
|
||||||
|
'On exchange stoploss is not supported for %s.' % self.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Checks if order time in force configured in strategy/config are supported
|
||||||
|
"""
|
||||||
|
if any(v != 'gtc' for k, v in order_time_in_force.items()):
|
||||||
|
if self.name is not 'Binance':
|
||||||
|
raise OperationalException(
|
||||||
|
f'Time in force policies are not supporetd for {self.name} yet.')
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
|
@ -241,14 +281,15 @@ class Exchange(object):
|
||||||
price = ceil(big_price) / pow(10, symbol_prec)
|
price = ceil(big_price) / pow(10, symbol_prec)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
def buy(self, pair: str, ordertype: str, amount: float,
|
||||||
|
rate: float, time_in_force) -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
self._dry_run_open_orders[order_id] = {
|
self._dry_run_open_orders[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'type': 'limit',
|
'type': ordertype,
|
||||||
'side': 'buy',
|
'side': 'buy',
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
@ -260,9 +301,14 @@ class Exchange(object):
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
rate = self.symbol_price_prec(pair, rate)
|
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||||
|
|
||||||
|
if time_in_force == 'gtc':
|
||||||
|
return self._api.create_order(pair, ordertype, 'buy', amount, rate)
|
||||||
|
else:
|
||||||
|
return self._api.create_order(pair, ordertype, 'buy',
|
||||||
|
amount, rate, {'timeInForce': time_in_force})
|
||||||
|
|
||||||
return self._api.create_limit_buy_order(pair, amount, rate)
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Insufficient funds to create limit buy order on market {pair}.'
|
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||||
|
@ -279,14 +325,15 @@ class Exchange(object):
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
def sell(self, pair: str, rate: float, amount: float) -> Dict:
|
def sell(self, pair: str, ordertype: str, amount: float,
|
||||||
|
rate: float, time_in_force='gtc') -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||||
self._dry_run_open_orders[order_id] = {
|
self._dry_run_open_orders[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'price': rate,
|
'price': rate,
|
||||||
'amount': amount,
|
'amount': amount,
|
||||||
'type': 'limit',
|
'type': ordertype,
|
||||||
'side': 'sell',
|
'side': 'sell',
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'datetime': arrow.utcnow().isoformat(),
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
@ -297,9 +344,14 @@ class Exchange(object):
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
amount = self.symbol_amount_prec(pair, amount)
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
rate = self.symbol_price_prec(pair, rate)
|
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||||
|
|
||||||
|
if time_in_force == 'gtc':
|
||||||
|
return self._api.create_order(pair, ordertype, 'sell', amount, rate)
|
||||||
|
else:
|
||||||
|
return self._api.create_order(pair, ordertype, 'sell',
|
||||||
|
amount, rate, {'timeInForce': time_in_force})
|
||||||
|
|
||||||
return self._api.create_limit_sell_order(pair, amount, rate)
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'Insufficient funds to create limit sell order on market {pair}.'
|
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||||
|
@ -316,6 +368,61 @@ class Exchange(object):
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||||
|
"""
|
||||||
|
creates a stoploss limit order.
|
||||||
|
NOTICE: it is not supported by all exchanges. only binance is tested for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
|
rate = self.symbol_price_prec(pair, rate)
|
||||||
|
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||||
|
|
||||||
|
# Ensure rate is less than stop price
|
||||||
|
if stop_price <= rate:
|
||||||
|
raise OperationalException(
|
||||||
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
|
||||||
|
if self._conf['dry_run']:
|
||||||
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
|
self._dry_run_open_orders[order_id] = {
|
||||||
|
'info': {},
|
||||||
|
'id': order_id,
|
||||||
|
'pair': pair,
|
||||||
|
'price': stop_price,
|
||||||
|
'amount': amount,
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'side': 'sell',
|
||||||
|
'remaining': amount,
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'status': 'open',
|
||||||
|
'fee': None
|
||||||
|
}
|
||||||
|
return self._dry_run_open_orders[order_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||||
|
amount, rate, {'stopPrice': stop_price})
|
||||||
|
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to place stoploss limit order on market {pair}. '
|
||||||
|
f'Tried to put a stoploss amount {amount} with '
|
||||||
|
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Could not place stoploss limit order on market {pair}.'
|
||||||
|
f'Tried to place stoploss amount {amount} with '
|
||||||
|
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
|
@ -367,6 +474,8 @@ class Exchange(object):
|
||||||
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
||||||
if refresh or pair not in self._cached_ticker.keys():
|
if refresh or pair not in self._cached_ticker.keys():
|
||||||
try:
|
try:
|
||||||
|
if pair not in self._api.markets:
|
||||||
|
raise DependencyException(f"Pair {pair} not available")
|
||||||
data = self._api.fetch_ticker(pair)
|
data = self._api.fetch_ticker(pair)
|
||||||
try:
|
try:
|
||||||
self._cached_ticker[pair] = {
|
self._cached_ticker[pair] = {
|
||||||
|
@ -410,9 +519,9 @@ class Exchange(object):
|
||||||
|
|
||||||
# Combine tickers
|
# Combine tickers
|
||||||
data: List = []
|
data: List = []
|
||||||
for tick in tickers:
|
for p, ticker in tickers:
|
||||||
if tick[0] == pair:
|
if p == pair:
|
||||||
data.extend(tick[1])
|
data.extend(ticker)
|
||||||
# Sort data again after extending the result - above calls return in "async order" order
|
# Sort data again after extending the result - above calls return in "async order" order
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
logger.info("downloaded %s with length %s.", pair, len(data))
|
logger.info("downloaded %s with length %s.", pair, len(data))
|
||||||
|
@ -420,7 +529,7 @@ class Exchange(object):
|
||||||
|
|
||||||
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
|
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
|
||||||
"""
|
"""
|
||||||
Refresh tickers asyncronously and return the result.
|
Refresh tickers asyncronously and set `_klines` of this object with the result
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing klines for %d pairs", len(pair_list))
|
logger.debug("Refreshing klines for %d pairs", len(pair_list))
|
||||||
asyncio.get_event_loop().run_until_complete(
|
asyncio.get_event_loop().run_until_complete(
|
||||||
|
@ -429,9 +538,27 @@ class Exchange(object):
|
||||||
async def async_get_candles_history(self, pairs: List[str],
|
async def async_get_candles_history(self, pairs: List[str],
|
||||||
tick_interval: str) -> List[Tuple[str, List]]:
|
tick_interval: str) -> List[Tuple[str, List]]:
|
||||||
"""Download ohlcv history for pair-list asyncronously """
|
"""Download ohlcv history for pair-list asyncronously """
|
||||||
input_coroutines = [self._async_get_candle_history(
|
# Calculating ticker interval in second
|
||||||
symbol, tick_interval) for symbol in pairs]
|
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
|
||||||
|
input_coroutines = []
|
||||||
|
|
||||||
|
# Gather corotines to run
|
||||||
|
for pair in pairs:
|
||||||
|
if not (self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
|
||||||
|
arrow.utcnow().timestamp and pair in self._klines):
|
||||||
|
input_coroutines.append(self._async_get_candle_history(pair, tick_interval))
|
||||||
|
else:
|
||||||
|
logger.debug("Using cached klines data for %s ...", pair)
|
||||||
|
|
||||||
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||||
|
|
||||||
|
# handle caching
|
||||||
|
for pair, ticks in tickers:
|
||||||
|
# keeping last candle time as last refreshed time of the pair
|
||||||
|
if ticks:
|
||||||
|
self._pairs_last_refresh_time[pair] = ticks[-1][0] // 1000
|
||||||
|
# keeping parsed dataframe in cache
|
||||||
|
self._klines[pair] = parse_ticker_dataframe(ticks)
|
||||||
return tickers
|
return tickers
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
|
@ -441,33 +568,16 @@ class Exchange(object):
|
||||||
# fetch ohlcv asynchronously
|
# fetch ohlcv asynchronously
|
||||||
logger.debug("fetching %s since %s ...", pair, since_ms)
|
logger.debug("fetching %s since %s ...", pair, since_ms)
|
||||||
|
|
||||||
# Calculating ticker interval in second
|
|
||||||
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
|
|
||||||
|
|
||||||
# If (last update time) + (interval in second) is greater or equal than now
|
|
||||||
# that means we don't have to hit the API as there is no new candle
|
|
||||||
# so we fetch it from local cache
|
|
||||||
if (not since_ms and
|
|
||||||
self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
|
|
||||||
arrow.utcnow().timestamp):
|
|
||||||
data = self.klines[pair]
|
|
||||||
logger.debug("Using cached klines data for %s ...", pair)
|
|
||||||
else:
|
|
||||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
||||||
since=since_ms)
|
since=since_ms)
|
||||||
|
|
||||||
# Because some exchange sort Tickers ASC and other DESC.
|
# Because some exchange sort Tickers ASC and other DESC.
|
||||||
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||||
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||||
|
# Only sort if necessary to save computing time
|
||||||
|
if data and data[0][0] > data[-1][0]:
|
||||||
data = sorted(data, key=lambda x: x[0])
|
data = sorted(data, key=lambda x: x[0])
|
||||||
|
|
||||||
# keeping last candle time as last refreshed time of the pair
|
|
||||||
if data:
|
|
||||||
self._pairs_last_refresh_time[pair] = data[-1][0] // 1000
|
|
||||||
|
|
||||||
# keeping candles in cache
|
|
||||||
self.klines[pair] = data
|
|
||||||
|
|
||||||
logger.debug("done fetching %s ...", pair)
|
logger.debug("done fetching %s ...", pair)
|
||||||
return pair, data
|
return pair, data
|
||||||
|
|
||||||
|
@ -481,51 +591,6 @@ class Exchange(object):
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||||
|
|
||||||
@retrier
|
|
||||||
def get_candle_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(
|
|
||||||
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
|
@retrier
|
||||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
|
|
|
@ -12,17 +12,18 @@ from typing import Any, Callable, Dict, List, Optional
|
||||||
import arrow
|
import arrow
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from cachetools import TTLCache, cached
|
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException,
|
from freqtrade import (DependencyException, OperationalException,
|
||||||
TemporaryError, __version__, constants, persistence)
|
TemporaryError, __version__, constants, persistence)
|
||||||
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
|
from freqtrade.resolvers import StrategyResolver, PairListResolver
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType, IStrategy
|
||||||
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
from freqtrade.wallets import Wallets
|
||||||
from freqtrade.exchange.exchange_helpers import order_book_to_dataframe
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -51,9 +52,19 @@ class FreqtradeBot(object):
|
||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
self.config = config
|
||||||
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
|
self.wallets = Wallets(self.exchange)
|
||||||
|
pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList')
|
||||||
|
self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist
|
||||||
|
|
||||||
|
# Initializing Edge only if enabled
|
||||||
|
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||||
|
self.config.get('edge', {}).get('enabled', False) else None
|
||||||
|
|
||||||
|
self.active_pair_whitelist: List[str] = self.config['exchange']['pair_whitelist']
|
||||||
self._init_modules()
|
self._init_modules()
|
||||||
|
|
||||||
def _init_modules(self) -> None:
|
def _init_modules(self) -> None:
|
||||||
|
@ -97,7 +108,7 @@ class FreqtradeBot(object):
|
||||||
})
|
})
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
if state == State.RUNNING:
|
if state == State.RUNNING:
|
||||||
self._startup_messages()
|
self.rpc.startup_messages(self.config, self.pairlists)
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
@ -107,45 +118,10 @@ class FreqtradeBot(object):
|
||||||
constants.PROCESS_THROTTLE_SECS
|
constants.PROCESS_THROTTLE_SECS
|
||||||
)
|
)
|
||||||
|
|
||||||
nb_assets = self.config.get('dynamic_whitelist', None)
|
|
||||||
|
|
||||||
self._throttle(func=self._process,
|
self._throttle(func=self._process,
|
||||||
min_secs=min_secs,
|
min_secs=min_secs)
|
||||||
nb_assets=nb_assets)
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def _startup_messages(self) -> None:
|
|
||||||
if self.config.get('dry_run', False):
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
|
||||||
'status': 'Dry run is enabled. All trades are simulated.'
|
|
||||||
})
|
|
||||||
stake_currency = self.config['stake_currency']
|
|
||||||
stake_amount = self.config['stake_amount']
|
|
||||||
minimal_roi = self.config['minimal_roi']
|
|
||||||
ticker_interval = self.config['ticker_interval']
|
|
||||||
exchange_name = self.config['exchange']['name']
|
|
||||||
strategy_name = self.config.get('strategy', '')
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
|
||||||
'status': f'*Exchange:* `{exchange_name}`\n'
|
|
||||||
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
|
||||||
f'*Minimum ROI:* `{minimal_roi}`\n'
|
|
||||||
f'*Ticker Interval:* `{ticker_interval}`\n'
|
|
||||||
f'*Strategy:* `{strategy_name}`'
|
|
||||||
})
|
|
||||||
if self.config.get('dynamic_whitelist', False):
|
|
||||||
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
|
|
||||||
specific_pairs = ''
|
|
||||||
else:
|
|
||||||
top_pairs = 'whitelisted'
|
|
||||||
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
|
|
||||||
self.rpc.send_msg({
|
|
||||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
|
||||||
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
|
|
||||||
f'{specific_pairs}'
|
|
||||||
})
|
|
||||||
|
|
||||||
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Throttles the given callable that it
|
Throttles the given callable that it
|
||||||
|
@ -162,32 +138,37 @@ class FreqtradeBot(object):
|
||||||
time.sleep(duration)
|
time.sleep(duration)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _process(self, nb_assets: Optional[int] = 0) -> bool:
|
def _process(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
otherwise a new trade is created.
|
otherwise a new trade is created.
|
||||||
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
|
||||||
:return: True if one or more trades has been created or closed, False otherwise
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
state_changed = False
|
state_changed = False
|
||||||
try:
|
try:
|
||||||
# Refresh whitelist based on wallet maintenance
|
# Refresh whitelist
|
||||||
sanitized_list = self._refresh_whitelist(
|
self.pairlists.refresh_pairlist()
|
||||||
self._gen_pair_whitelist(
|
self.active_pair_whitelist = self.pairlists.whitelist
|
||||||
self.config['stake_currency']
|
|
||||||
) if nb_assets else self.config['exchange']['pair_whitelist']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep only the subsets of pairs wanted (up to nb_assets)
|
# Calculating Edge positiong
|
||||||
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
# Should be called before refresh_tickers
|
||||||
self.config['exchange']['pair_whitelist'] = final_list
|
# Otherwise it will override cached klines in exchange
|
||||||
|
# with delta value (klines only from last refresh_pairs)
|
||||||
# Refreshing candles
|
if self.edge:
|
||||||
self.exchange.refresh_tickers(final_list, self.strategy.ticker_interval)
|
self.edge.calculate()
|
||||||
|
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
# Extend active-pair whitelist with pairs from open trades
|
||||||
|
# ensures that tickers are downloaded for open trades
|
||||||
|
self.active_pair_whitelist.extend([trade.pair for trade in trades
|
||||||
|
if trade.pair not in self.active_pair_whitelist])
|
||||||
|
|
||||||
|
# Refreshing candles
|
||||||
|
self.exchange.refresh_tickers(self.active_pair_whitelist, self.strategy.ticker_interval)
|
||||||
|
|
||||||
# First process current opened trades
|
# First process current opened trades
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
state_changed |= self.process_maybe_execute_sell(trade)
|
state_changed |= self.process_maybe_execute_sell(trade)
|
||||||
|
@ -215,63 +196,6 @@ class FreqtradeBot(object):
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
|
||||||
def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]:
|
|
||||||
"""
|
|
||||||
Updates the whitelist with with a dynamically generated list
|
|
||||||
:param base_currency: base currency as str
|
|
||||||
:param key: sort key (defaults to 'quoteVolume')
|
|
||||||
:return: List of pairs
|
|
||||||
"""
|
|
||||||
|
|
||||||
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]:
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
trade
|
|
||||||
:return: the list of pairs the user wants to trade without the one unavailable or
|
|
||||||
black_listed
|
|
||||||
"""
|
|
||||||
sanitized_whitelist = whitelist
|
|
||||||
markets = self.exchange.get_markets()
|
|
||||||
|
|
||||||
markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
|
|
||||||
known_pairs = set()
|
|
||||||
for market in markets:
|
|
||||||
pair = market['symbol']
|
|
||||||
# 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', []):
|
|
||||||
continue
|
|
||||||
# else the pair is valid
|
|
||||||
known_pairs.add(pair)
|
|
||||||
# Market is not active
|
|
||||||
if not market['active']:
|
|
||||||
sanitized_whitelist.remove(pair)
|
|
||||||
logger.info(
|
|
||||||
'Ignoring %s from whitelist. Market is not active.',
|
|
||||||
pair
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to remove pairs that are unknown
|
|
||||||
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
|
||||||
|
|
||||||
return final_list
|
|
||||||
|
|
||||||
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
|
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid target between current ask price and last price
|
Calculates bid target between current ask price and last price
|
||||||
|
@ -307,14 +231,23 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
return used_rate
|
return used_rate
|
||||||
|
|
||||||
def _get_trade_stake_amount(self) -> Optional[float]:
|
def _get_trade_stake_amount(self, pair) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Check if stake amount can be fulfilled with the available balance
|
Check if stake amount can be fulfilled with the available balance
|
||||||
for the stake currency
|
for the stake currency
|
||||||
:return: float: Stake Amount
|
:return: float: Stake Amount
|
||||||
"""
|
"""
|
||||||
|
if self.edge:
|
||||||
|
return self.edge.stake_amount(
|
||||||
|
pair,
|
||||||
|
self.wallets.get_free(self.config['stake_currency']),
|
||||||
|
self.wallets.get_total(self.config['stake_currency']),
|
||||||
|
Trade.total_open_trades_stakes()
|
||||||
|
)
|
||||||
|
else:
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
|
||||||
|
avaliable_amount = self.wallets.get_free(self.config['stake_currency'])
|
||||||
|
|
||||||
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
||||||
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
|
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
|
||||||
|
@ -371,16 +304,7 @@ class FreqtradeBot(object):
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
"""
|
"""
|
||||||
interval = self.strategy.ticker_interval
|
interval = self.strategy.ticker_interval
|
||||||
stake_amount = self._get_trade_stake_amount()
|
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||||
|
|
||||||
if not stake_amount:
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
|
||||||
stake_amount
|
|
||||||
)
|
|
||||||
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
# 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():
|
||||||
|
@ -392,10 +316,18 @@ class FreqtradeBot(object):
|
||||||
raise DependencyException('No currency pairs in whitelist')
|
raise DependencyException('No currency pairs in whitelist')
|
||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
# to find buy signals
|
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines.get(_pair))
|
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines(_pair))
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
|
stake_amount = self._get_trade_stake_amount(_pair)
|
||||||
|
if not stake_amount:
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Buy signal found: about create a new trade with stake_amount: %f ...',
|
||||||
|
stake_amount
|
||||||
|
)
|
||||||
|
|
||||||
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
||||||
get('check_depth_of_market', {})
|
get('check_depth_of_market', {})
|
||||||
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
||||||
|
@ -425,7 +357,7 @@ class FreqtradeBot(object):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
|
@ -435,11 +367,15 @@ class FreqtradeBot(object):
|
||||||
pair_url = self.exchange.get_pair_detail_url(pair)
|
pair_url = self.exchange.get_pair_detail_url(pair)
|
||||||
stake_currency = self.config['stake_currency']
|
stake_currency = self.config['stake_currency']
|
||||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||||
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
|
if price:
|
||||||
|
buy_limit_requested = price
|
||||||
|
else:
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
buy_limit_requested = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||||
|
|
||||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
|
||||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'Can\'t open a new trade for {pair_s}: stake amount'
|
f'Can\'t open a new trade for {pair_s}: stake amount'
|
||||||
|
@ -447,20 +383,59 @@ class FreqtradeBot(object):
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
amount = stake_amount / buy_limit
|
amount = stake_amount / buy_limit_requested
|
||||||
|
|
||||||
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'],
|
||||||
|
amount=amount, rate=buy_limit_requested,
|
||||||
|
time_in_force=time_in_force)
|
||||||
|
order_id = order['id']
|
||||||
|
order_status = order.get('status', None)
|
||||||
|
|
||||||
|
# we assume the order is executed at the price requested
|
||||||
|
buy_limit_filled_price = buy_limit_requested
|
||||||
|
|
||||||
|
if order_status == 'expired' or order_status == 'rejected':
|
||||||
|
order_type = self.strategy.order_types['buy']
|
||||||
|
order_tif = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
|
# return false if the order is not filled
|
||||||
|
if float(order['filled']) == 0:
|
||||||
|
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||||
|
' zero amount is fulfilled.',
|
||||||
|
order_tif, order_type, pair_s, order_status, self.exchange.name)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# the order is partially fulfilled
|
||||||
|
# in case of IOC orders we can check immediately
|
||||||
|
# if the order is fulfilled fully or partially
|
||||||
|
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
||||||
|
' %s amount fulfilled out of %s (%s remaining which is canceled).',
|
||||||
|
order_tif, order_type, pair_s, order_status, self.exchange.name,
|
||||||
|
order['filled'], order['amount'], order['remaining']
|
||||||
|
)
|
||||||
|
stake_amount = order['cost']
|
||||||
|
amount = order['amount']
|
||||||
|
buy_limit_filled_price = order['price']
|
||||||
|
order_id = None
|
||||||
|
|
||||||
|
# in case of FOK the order may be filled immediately and fully
|
||||||
|
elif order_status == 'closed':
|
||||||
|
stake_amount = order['cost']
|
||||||
|
amount = order['amount']
|
||||||
|
buy_limit_filled_price = order['price']
|
||||||
|
order_id = None
|
||||||
|
|
||||||
self.rpc.send_msg({
|
self.rpc.send_msg({
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': pair_s,
|
'pair': pair_s,
|
||||||
'market_url': pair_url,
|
'market_url': pair_url,
|
||||||
'limit': buy_limit,
|
'limit': buy_limit_filled_price,
|
||||||
'stake_amount': stake_amount,
|
'stake_amount': stake_amount,
|
||||||
'stake_currency': stake_currency,
|
'stake_currency': stake_currency,
|
||||||
'fiat_currency': fiat_currency
|
'fiat_currency': fiat_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')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -469,16 +444,21 @@ class FreqtradeBot(object):
|
||||||
amount=amount,
|
amount=amount,
|
||||||
fee_open=fee,
|
fee_open=fee,
|
||||||
fee_close=fee,
|
fee_close=fee,
|
||||||
open_rate=buy_limit,
|
open_rate=buy_limit_filled_price,
|
||||||
open_rate_requested=buy_limit,
|
open_rate_requested=buy_limit_requested,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
strategy=self.strategy.get_strategy_name(),
|
strategy=self.strategy.get_strategy_name(),
|
||||||
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
||||||
)
|
)
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
# Updating wallets
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_maybe_execute_buy(self) -> bool:
|
def process_maybe_execute_buy(self) -> bool:
|
||||||
|
@ -521,9 +501,22 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
|
|
||||||
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open:
|
||||||
|
result = self.handle_stoploss_on_exchange(trade)
|
||||||
|
if result:
|
||||||
|
self.wallets.update()
|
||||||
|
return result
|
||||||
|
|
||||||
if trade.is_open and trade.open_order_id is None:
|
if trade.is_open and trade.open_order_id is None:
|
||||||
# Check if we can sell our current pair
|
# Check if we can sell our current pair
|
||||||
return self.handle_trade(trade)
|
result = self.handle_trade(trade)
|
||||||
|
|
||||||
|
# Updating wallets if any trade occured
|
||||||
|
if result:
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to sell trade: %s', exception)
|
logger.warning('Unable to sell trade: %s', exception)
|
||||||
return False
|
return False
|
||||||
|
@ -585,9 +578,8 @@ class FreqtradeBot(object):
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||||
ticker = self.exchange.klines.get(trade.pair)
|
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
||||||
ticker)
|
self.exchange.klines(trade.pair))
|
||||||
|
|
||||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
if config_ask_strategy.get('use_order_book', False):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
|
@ -611,18 +603,65 @@ class FreqtradeBot(object):
|
||||||
return True
|
return True
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
logger.info('checking sell')
|
logger.debug('checking sell')
|
||||||
if self.check_sell(trade, sell_rate, buy, sell):
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Check if trade is fulfilled in which case the stoploss
|
||||||
|
on exchange should be added immediately if stoploss on exchnage
|
||||||
|
is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = False
|
||||||
|
|
||||||
|
# If trade is open and the buy order is fulfilled but there is no stoploss,
|
||||||
|
# then we add a stoploss on exchange
|
||||||
|
if not trade.open_order_id and not trade.stoploss_order_id:
|
||||||
|
if self.edge:
|
||||||
|
stoploss = self.edge.stoploss(pair=trade.pair)
|
||||||
|
else:
|
||||||
|
stoploss = self.strategy.stoploss
|
||||||
|
|
||||||
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
|
# limit price should be less than stop price.
|
||||||
|
# 0.98 is arbitrary here.
|
||||||
|
limit_price = stop_price * 0.98
|
||||||
|
|
||||||
|
stoploss_order_id = self.exchange.stoploss_limit(
|
||||||
|
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||||
|
)['id']
|
||||||
|
trade.stoploss_order_id = str(stoploss_order_id)
|
||||||
|
|
||||||
|
# Or the trade open and there is already a stoploss on exchange.
|
||||||
|
# so we check if it is hit ...
|
||||||
|
elif trade.stoploss_order_id:
|
||||||
|
logger.debug('Handling stoploss on exchange %s ...', trade)
|
||||||
|
order = self.exchange.get_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
if order['status'] == 'closed':
|
||||||
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
trade.update(order)
|
||||||
|
result = True
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
|
if self.edge:
|
||||||
|
stoploss = self.edge.stoploss(trade.pair)
|
||||||
|
should_sell = self.strategy.should_sell(
|
||||||
|
trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss)
|
||||||
|
else:
|
||||||
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
|
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
|
||||||
|
|
||||||
if should_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||||
logger.info('excuted sell')
|
logger.info('executed sell, reason: %s', should_sell.sell_type)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -655,15 +694,18 @@ class FreqtradeBot(object):
|
||||||
ordertime = arrow.get(order['datetime']).datetime
|
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 float(order['remaining']) == 0.0:
|
||||||
|
self.wallets.update()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if trade is still actually open
|
# Check if trade is still actually open
|
||||||
if order['status'] == 'open':
|
if order['status'] == 'open':
|
||||||
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||||
self.handle_timedout_limit_buy(trade, order)
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
|
self.wallets.update()
|
||||||
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
||||||
self.handle_timedout_limit_sell(trade, order)
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
# it is conditionally called in the
|
# it is conditionally called in the
|
||||||
|
@ -730,8 +772,27 @@ class FreqtradeBot(object):
|
||||||
:param sellreason: Reason the sell was triggered
|
:param sellreason: Reason the sell was triggered
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
sell_type = 'sell'
|
||||||
|
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
|
sell_type = 'stoploss'
|
||||||
|
|
||||||
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
|
# we consider the sell price stop price
|
||||||
|
if self.config.get('dry_run', False) and sell_type == 'stoploss' \
|
||||||
|
and self.strategy.order_types['stoploss_on_exchange']:
|
||||||
|
limit = trade.stop_loss
|
||||||
|
|
||||||
|
# First cancelling stoploss on exchange ...
|
||||||
|
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
|
||||||
|
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
|
||||||
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
order_id = self.exchange.sell(pair=str(trade.pair),
|
||||||
|
ordertype=self.strategy.order_types[sell_type],
|
||||||
|
amount=trade.amount, rate=limit,
|
||||||
|
time_in_force=self.strategy.order_time_in_force['sell']
|
||||||
|
)['id']
|
||||||
|
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
trade.sell_reason = sell_reason.value
|
trade.sell_reason = sell_reason.value
|
||||||
|
@ -754,6 +815,7 @@ class FreqtradeBot(object):
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_trade,
|
'profit_amount': profit_trade,
|
||||||
'profit_percent': profit_percent,
|
'profit_percent': profit_percent,
|
||||||
|
'sell_reason': sell_reason.value
|
||||||
}
|
}
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# For regular case, when the configuration exists
|
||||||
|
|
|
@ -1,245 +1,49 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
import gzip
|
|
||||||
try:
|
|
||||||
import ujson as json
|
|
||||||
_UJSON = True
|
|
||||||
except ImportError:
|
|
||||||
# see mypy/issues/1153
|
|
||||||
import json # type: ignore
|
|
||||||
_UJSON = False
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
from datetime import datetime
|
||||||
from typing import Optional, List, Dict, Tuple, Any
|
from typing import Dict, Tuple
|
||||||
import arrow
|
import operator
|
||||||
|
|
||||||
from freqtrade import misc, constants, OperationalException
|
import arrow
|
||||||
from freqtrade.exchange import Exchange
|
from pandas import DataFrame
|
||||||
from freqtrade.arguments import TimeRange
|
|
||||||
|
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts # noqa: F401
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def json_load(data):
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
"""Try to load data with ujson"""
|
|
||||||
if _UJSON:
|
|
||||||
return json.load(data, precise_float=True)
|
|
||||||
else:
|
|
||||||
return json.load(data)
|
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
|
||||||
if not tickerlist:
|
|
||||||
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(
|
|
||||||
datadir: str, pair: str,
|
|
||||||
ticker_interval: str,
|
|
||||||
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
|
|
||||||
"""
|
"""
|
||||||
Load a pair from file,
|
Get the maximum timeframe for the given backtest data
|
||||||
:return dict OR empty if unsuccesful
|
:param data: dictionary with preprocessed backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
"""
|
"""
|
||||||
path = make_testdata_path(datadir)
|
timeframe = [
|
||||||
pair_s = pair.replace('/', '_')
|
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
||||||
file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
|
for frame in data.values()
|
||||||
gzipfile = file + '.gz'
|
]
|
||||||
|
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||||
# If the file does not exist we download it when None is returned.
|
max(timeframe, key=operator.itemgetter(1))[1]
|
||||||
# If file exists, read the file, load the json
|
|
||||||
if os.path.isfile(gzipfile):
|
|
||||||
logger.debug('Loading ticker data from file %s', gzipfile)
|
|
||||||
with gzip.open(gzipfile) as tickerdata:
|
|
||||||
pairdata = json.load(tickerdata)
|
|
||||||
elif os.path.isfile(file):
|
|
||||||
logger.debug('Loading ticker data from file %s', file)
|
|
||||||
with open(file) as tickerdata:
|
|
||||||
pairdata = json.load(tickerdata)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if timerange:
|
|
||||||
pairdata = trim_tickerlist(pairdata, timerange)
|
|
||||||
return pairdata
|
|
||||||
|
|
||||||
|
|
||||||
def load_data(datadir: str,
|
def validate_backtest_data(data: Dict[str, DataFrame], min_date: datetime,
|
||||||
ticker_interval: str,
|
max_date: datetime, ticker_interval_mins: int) -> bool:
|
||||||
pairs: List[str],
|
|
||||||
refresh_pairs: Optional[bool] = False,
|
|
||||||
exchange: Optional[Exchange] = None,
|
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Validates preprocessed backtesting data for missing values and shows warnings about it that.
|
||||||
:return: dict
|
|
||||||
|
:param data: dictionary with preprocessed backtesting data
|
||||||
|
:param min_date: start-date of the data
|
||||||
|
:param max_date: end-date of the data
|
||||||
|
:param ticker_interval_mins: ticker interval in minutes
|
||||||
"""
|
"""
|
||||||
result = {}
|
# total difference in minutes / interval-minutes
|
||||||
|
expected_frames = int((max_date - min_date).total_seconds() // 60 // ticker_interval_mins)
|
||||||
# If the user force the refresh of pairs
|
found_missing = False
|
||||||
if refresh_pairs:
|
for pair, df in data.items():
|
||||||
logger.info('Download data for all pairs and store them in %s', datadir)
|
dflen = len(df)
|
||||||
if not exchange:
|
if dflen < expected_frames:
|
||||||
raise OperationalException("Exchange needs to be initialized when "
|
found_missing = True
|
||||||
"calling load_data with refresh_pairs=True")
|
logger.warning("%s has missing frames: expected %s, got %s, that's %s missing values",
|
||||||
download_pairs(datadir, exchange, pairs, ticker_interval, timerange=timerange)
|
pair, expected_frames, dflen, expected_frames - dflen)
|
||||||
|
return found_missing
|
||||||
for pair in pairs:
|
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
|
||||||
if pairdata:
|
|
||||||
result[pair] = pairdata
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
'No data for pair: "%s", Interval: %s. '
|
|
||||||
'Use --refresh-pairs-cached to download the data',
|
|
||||||
pair,
|
|
||||||
ticker_interval
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def make_testdata_path(datadir: str) -> str:
|
|
||||||
"""Return the path where testdata files are stored"""
|
|
||||||
return datadir or os.path.abspath(
|
|
||||||
os.path.join(
|
|
||||||
os.path.dirname(__file__), '..', 'tests', 'testdata'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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 pair in pairs:
|
|
||||||
try:
|
|
||||||
download_backtesting_testdata(datadir,
|
|
||||||
exchange=exchange,
|
|
||||||
pair=pair,
|
|
||||||
tick_interval=ticker_interval,
|
|
||||||
timerange=timerange)
|
|
||||||
except BaseException:
|
|
||||||
logger.info(
|
|
||||||
'Failed to download the pair: "%s", Interval: %s',
|
|
||||||
pair,
|
|
||||||
ticker_interval
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def load_cached_data_for_updating(filename: str,
|
|
||||||
tick_interval: str,
|
|
||||||
timerange: Optional[TimeRange]) -> Tuple[
|
|
||||||
List[Any],
|
|
||||||
Optional[int]]:
|
|
||||||
"""
|
|
||||||
Load cached data and choose what part of the data should be updated
|
|
||||||
"""
|
|
||||||
|
|
||||||
since_ms = None
|
|
||||||
|
|
||||||
# user sets timerange, so find the start time
|
|
||||||
if timerange:
|
|
||||||
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):
|
|
||||||
with open(filename, "rt") as 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:
|
|
||||||
data = []
|
|
||||||
|
|
||||||
if data:
|
|
||||||
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
|
|
||||||
|
|
||||||
return (data, since_ms)
|
|
||||||
|
|
||||||
|
|
||||||
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 pair passed in parameters
|
|
||||||
The data is downloaded starting from the last correct ticker interval data that
|
|
||||||
exists 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 pair: pair 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')
|
|
||||||
|
|
||||||
# Default since_ms to 30 days if nothing is given
|
|
||||||
new_data = exchange.get_history(pair=pair, tick_interval=tick_interval,
|
|
||||||
since_ms=since_ms if since_ms
|
|
||||||
else
|
|
||||||
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
|
||||||
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]))
|
|
||||||
|
|
||||||
misc.file_dump_json(filename, data)
|
|
||||||
|
|
|
@ -4,14 +4,12 @@
|
||||||
This module contains the backtesting logic
|
This module contains the backtesting logic
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import operator
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
@ -20,10 +18,11 @@ from freqtrade import DependencyException, constants
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.data import history
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
from freqtrade.strategy.interface import SellType, IStrategy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -68,6 +67,7 @@ class Backtesting(object):
|
||||||
if self.config.get('strategy_list', None):
|
if self.config.get('strategy_list', None):
|
||||||
# Force one interval
|
# Force one interval
|
||||||
self.ticker_interval = str(self.config.get('ticker_interval'))
|
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||||
|
self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]
|
||||||
for strat in list(self.config['strategy_list']):
|
for strat in list(self.config['strategy_list']):
|
||||||
stratconf = deepcopy(self.config)
|
stratconf = deepcopy(self.config)
|
||||||
stratconf['strategy'] = strat
|
stratconf['strategy'] = strat
|
||||||
|
@ -88,24 +88,11 @@ class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
self.ticker_interval = self.config.get('ticker_interval')
|
self.ticker_interval = self.config.get('ticker_interval')
|
||||||
|
self.ticker_interval_mins = constants.TICKER_INTERVAL_MINUTES[self.ticker_interval]
|
||||||
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
||||||
self.advise_buy = strategy.advise_buy
|
self.advise_buy = strategy.advise_buy
|
||||||
self.advise_sell = strategy.advise_sell
|
self.advise_sell = strategy.advise_sell
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
|
||||||
"""
|
|
||||||
Get the maximum timeframe for the given backtest data
|
|
||||||
:param data: dictionary with preprocessed backtesting data
|
|
||||||
:return: tuple containing min_date, max_date
|
|
||||||
"""
|
|
||||||
timeframe = [
|
|
||||||
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
|
||||||
for frame in data.values()
|
|
||||||
]
|
|
||||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
|
||||||
max(timeframe, key=operator.itemgetter(1))[1]
|
|
||||||
|
|
||||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
|
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
|
||||||
skip_nan: bool = False) -> str:
|
skip_nan: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -223,21 +210,37 @@ class Backtesting(object):
|
||||||
|
|
||||||
buy_signal = sell_row.buy
|
buy_signal = sell_row.buy
|
||||||
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
||||||
sell_row.sell)
|
sell_row.sell, low=sell_row.low, high=sell_row.high)
|
||||||
if sell.sell_flag:
|
if sell.sell_flag:
|
||||||
|
|
||||||
|
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
|
||||||
|
# Special handling if high or low hit STOP_LOSS or ROI
|
||||||
|
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
|
# Set close_rate to stoploss
|
||||||
|
closerate = trade.stop_loss
|
||||||
|
elif sell.sell_type == (SellType.ROI):
|
||||||
|
# get entry in min_roi >= to trade duration
|
||||||
|
roi_entry = max(list(filter(lambda x: trade_dur >= x,
|
||||||
|
self.strategy.minimal_roi.keys())))
|
||||||
|
roi = self.strategy.minimal_roi[roi_entry]
|
||||||
|
|
||||||
|
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
|
||||||
|
closerate = - (trade.open_rate * roi + trade.open_rate *
|
||||||
|
(1 + trade.fee_open)) / (trade.fee_close - 1)
|
||||||
|
else:
|
||||||
|
closerate = sell_row.open
|
||||||
|
|
||||||
return BacktestResult(pair=pair,
|
return BacktestResult(pair=pair,
|
||||||
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
profit_percent=trade.calc_profit_percent(rate=closerate),
|
||||||
profit_abs=trade.calc_profit(rate=sell_row.open),
|
profit_abs=trade.calc_profit(rate=closerate),
|
||||||
open_time=buy_row.date,
|
open_time=buy_row.date,
|
||||||
close_time=sell_row.date,
|
close_time=sell_row.date,
|
||||||
trade_duration=int((
|
trade_duration=trade_dur,
|
||||||
sell_row.date - buy_row.date).total_seconds() // 60),
|
|
||||||
open_index=buy_row.Index,
|
open_index=buy_row.Index,
|
||||||
close_index=sell_row.Index,
|
close_index=sell_row.Index,
|
||||||
open_at_end=False,
|
open_at_end=False,
|
||||||
open_rate=buy_row.open,
|
open_rate=buy_row.open,
|
||||||
close_rate=sell_row.open,
|
close_rate=closerate,
|
||||||
sell_reason=sell.sell_type
|
sell_reason=sell.sell_type
|
||||||
)
|
)
|
||||||
if partial_ticker:
|
if partial_ticker:
|
||||||
|
@ -277,12 +280,17 @@ class Backtesting(object):
|
||||||
position_stacking: do we allow position stacking? (default: False)
|
position_stacking: do we allow position stacking? (default: False)
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high']
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
position_stacking = args.get('position_stacking', False)
|
position_stacking = args.get('position_stacking', False)
|
||||||
|
start_date = args['start_date']
|
||||||
|
end_date = args['end_date']
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: Dict = {}
|
trade_count_lock: Dict = {}
|
||||||
|
ticker: Dict = {}
|
||||||
|
pairs = []
|
||||||
|
# Create ticker 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
|
||||||
|
|
||||||
|
@ -297,15 +305,28 @@ class Backtesting(object):
|
||||||
|
|
||||||
# Convert from Pandas to list for performance reasons
|
# Convert from Pandas to list for performance reasons
|
||||||
# (Looping Pandas is slow.)
|
# (Looping Pandas is slow.)
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||||
|
pairs.append(pair)
|
||||||
|
|
||||||
|
lock_pair_until: Dict = {}
|
||||||
|
tmp = start_date + timedelta(minutes=self.ticker_interval_mins)
|
||||||
|
index = 0
|
||||||
|
# Loop timerange and test per pair
|
||||||
|
while tmp < end_date:
|
||||||
|
# print(f"time: {tmp}")
|
||||||
|
for i, pair in enumerate(ticker):
|
||||||
|
try:
|
||||||
|
row = ticker[pair][index]
|
||||||
|
except IndexError:
|
||||||
|
# missing Data for one pair ...
|
||||||
|
# Warnings for this are shown by `validate_backtest_data`
|
||||||
|
continue
|
||||||
|
|
||||||
lock_pair_until = None
|
|
||||||
for index, row in enumerate(ticker):
|
|
||||||
if row.buy == 0 or row.sell == 1:
|
if row.buy == 0 or row.sell == 1:
|
||||||
continue # skip rows where no buy signal or that would immediately sell off
|
continue # skip rows where no buy signal or that would immediately sell off
|
||||||
|
|
||||||
if not position_stacking:
|
if not position_stacking:
|
||||||
if lock_pair_until is not None and row.date <= lock_pair_until:
|
if pair in lock_pair_until and row.date <= lock_pair_until[pair]:
|
||||||
continue
|
continue
|
||||||
if max_open_trades > 0:
|
if max_open_trades > 0:
|
||||||
# Check if max_open_trades has already been reached for the given date
|
# Check if max_open_trades has already been reached for the given date
|
||||||
|
@ -314,17 +335,19 @@ 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
|
||||||
|
|
||||||
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][index + 1:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, args)
|
||||||
|
|
||||||
if trade_entry:
|
if trade_entry:
|
||||||
lock_pair_until = trade_entry.close_time
|
lock_pair_until[pair] = trade_entry.close_time
|
||||||
trades.append(trade_entry)
|
trades.append(trade_entry)
|
||||||
else:
|
else:
|
||||||
# Set lock_pair_until to end of testing period if trade could not be closed
|
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||||
# This happens only if the buy-signal was with the last candle
|
# This happens only if the buy-signal was with the last candle
|
||||||
lock_pair_until = ticker_data.iloc[-1].date
|
lock_pair_until[pair] = end_date
|
||||||
|
|
||||||
|
tmp += timedelta(minutes=self.ticker_interval_mins)
|
||||||
|
index += 1
|
||||||
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
|
@ -340,14 +363,14 @@ class Backtesting(object):
|
||||||
if self.config.get('live'):
|
if self.config.get('live'):
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
self.exchange.refresh_tickers(pairs, self.ticker_interval)
|
self.exchange.refresh_tickers(pairs, self.ticker_interval)
|
||||||
data = self.exchange.klines
|
data = self.exchange._klines
|
||||||
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(None if self.config.get(
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = optimize.load_data(
|
data = history.load_data(
|
||||||
self.config['datadir'],
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
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),
|
||||||
|
@ -371,10 +394,12 @@ class Backtesting(object):
|
||||||
self._set_strategy(strat)
|
self._set_strategy(strat)
|
||||||
|
|
||||||
# need to reprocess data every time to populate signals
|
# need to reprocess data every time to populate signals
|
||||||
preprocessed = self.tickerdata_to_dataframe(data)
|
preprocessed = self.strategy.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
# Print timeframe
|
min_date, max_date = optimize.get_timeframe(preprocessed)
|
||||||
min_date, max_date = self.get_timeframe(preprocessed)
|
# Validate dataframe for missing values
|
||||||
|
optimize.validate_backtest_data(preprocessed, min_date, max_date,
|
||||||
|
constants.TICKER_INTERVAL_MINUTES[self.ticker_interval])
|
||||||
logger.info(
|
logger.info(
|
||||||
'Measuring data from %s up to %s (%s days)..',
|
'Measuring data from %s up to %s (%s days)..',
|
||||||
min_date.isoformat(),
|
min_date.isoformat(),
|
||||||
|
@ -389,6 +414,8 @@ class Backtesting(object):
|
||||||
'processed': preprocessed,
|
'processed': preprocessed,
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'position_stacking': self.config.get('position_stacking', False),
|
'position_stacking': self.config.get('position_stacking', False),
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
130
freqtrade/optimize/default_hyperopt.py
Normal file
130
freqtrade/optimize/default_hyperopt.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
from typing import Dict, Any, Callable, List
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
from skopt.space import Categorical, Dimension, Integer, Real
|
||||||
|
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
|
|
||||||
|
class_name = 'DefaultHyperOpts'
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultHyperOpts(IHyperOpt):
|
||||||
|
"""
|
||||||
|
Default hyperopt provided by freqtrade bot.
|
||||||
|
You can override it with your own hyperopt
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
|
# Bollinger bands
|
||||||
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Define the buy strategy parameters to be used by hyperopt
|
||||||
|
"""
|
||||||
|
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Buy strategy Hyperopt will build and use
|
||||||
|
"""
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||||
|
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||||
|
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||||
|
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||||
|
if 'adx-enabled' in params and params['adx-enabled']:
|
||||||
|
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||||
|
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||||
|
|
||||||
|
# TRIGGERS
|
||||||
|
if params['trigger'] == 'bb_lower':
|
||||||
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['macd'], dataframe['macdsignal']
|
||||||
|
))
|
||||||
|
if params['trigger'] == 'sar_reversal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['close'], dataframe['sar']
|
||||||
|
))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
return populate_buy_trend
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def indicator_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Define your Hyperopt space for searching strategy parameters
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(10, 25, name='mfi-value'),
|
||||||
|
Integer(15, 45, name='fastd-value'),
|
||||||
|
Integer(20, 50, name='adx-value'),
|
||||||
|
Integer(20, 40, name='rsi-value'),
|
||||||
|
Categorical([True, False], name='mfi-enabled'),
|
||||||
|
Categorical([True, False], name='fastd-enabled'),
|
||||||
|
Categorical([True, False], name='adx-enabled'),
|
||||||
|
Categorical([True, False], name='rsi-enabled'),
|
||||||
|
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
|
"""
|
||||||
|
Generate the ROI table that will be used by Hyperopt
|
||||||
|
"""
|
||||||
|
roi_table = {}
|
||||||
|
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||||
|
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||||
|
|
||||||
|
return roi_table
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def stoploss_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Stoploss Value to search
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Real(-0.5, -0.02, name='stoploss'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def roi_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Values to search for each ROI steps
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(10, 120, name='roi_t1'),
|
||||||
|
Integer(10, 60, name='roi_t2'),
|
||||||
|
Integer(10, 40, name='roi_t3'),
|
||||||
|
Real(0.01, 0.04, name='roi_p1'),
|
||||||
|
Real(0.01, 0.07, name='roi_p2'),
|
||||||
|
Real(0.01, 0.20, name='roi_p3'),
|
||||||
|
]
|
106
freqtrade/optimize/edge_cli.py
Normal file
106
freqtrade/optimize/edge_cli.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains the edge backtesting interface
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Dict, Any
|
||||||
|
from tabulate import tabulate
|
||||||
|
from freqtrade.edge import Edge
|
||||||
|
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EdgeCli(object):
|
||||||
|
"""
|
||||||
|
EdgeCli class, this class contains all the logic to run edge backtesting
|
||||||
|
|
||||||
|
To run a edge backtest:
|
||||||
|
edge = EdgeCli(config)
|
||||||
|
edge.start()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
# Reset keys for edge
|
||||||
|
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.strategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
|
self.edge = Edge(config, self.exchange, self.strategy)
|
||||||
|
self.edge._refresh_pairs = self.config.get('refresh_pairs', False)
|
||||||
|
|
||||||
|
self.timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
|
|
||||||
|
self.edge._timerange = self.timerange
|
||||||
|
|
||||||
|
def _generate_edge_table(self, results: dict) -> str:
|
||||||
|
|
||||||
|
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio',
|
||||||
|
'required risk reward', 'expectancy', 'total number of trades',
|
||||||
|
'average duration (min)']
|
||||||
|
|
||||||
|
for result in results.items():
|
||||||
|
if result[1].nb_trades > 0:
|
||||||
|
tabular_data.append([
|
||||||
|
result[0],
|
||||||
|
result[1].stoploss,
|
||||||
|
result[1].winrate,
|
||||||
|
result[1].risk_reward_ratio,
|
||||||
|
result[1].required_risk_reward,
|
||||||
|
result[1].expectancy,
|
||||||
|
result[1].nb_trades,
|
||||||
|
round(result[1].avg_trade_duration)
|
||||||
|
])
|
||||||
|
|
||||||
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
self.edge.calculate()
|
||||||
|
print('') # blank like for readability
|
||||||
|
print(self._generate_edge_table(self.edge._cached_pairs))
|
||||||
|
|
||||||
|
|
||||||
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Prepare the configuration for edge backtesting
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: Configuration
|
||||||
|
"""
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def start(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Start Edge script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Initialize configuration
|
||||||
|
config = setup_configuration(args)
|
||||||
|
logger.info('Starting freqtrade in Edge mode')
|
||||||
|
|
||||||
|
# Initialize Edge object
|
||||||
|
edge_cli = EdgeCli(config)
|
||||||
|
edge_cli.start()
|
|
@ -5,26 +5,27 @@ This module contains the hyperopt logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
from argparse import Namespace
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from pathlib import Path
|
||||||
from functools import reduce
|
|
||||||
from math import exp
|
from math import exp
|
||||||
|
import multiprocessing
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from sklearn.externals.joblib import Parallel, delayed, dump, load
|
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects
|
||||||
from skopt import Optimizer
|
from skopt import Optimizer
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real
|
from skopt.space import Dimension
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.optimize import load_data
|
from freqtrade.data.history import load_data
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from freqtrade.resolvers import HyperOptResolver
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -42,6 +43,9 @@ class Hyperopt(Backtesting):
|
||||||
"""
|
"""
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
self.config = config
|
||||||
|
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
|
||||||
|
|
||||||
# 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
|
||||||
self.target_trades = 600
|
self.target_trades = 600
|
||||||
|
@ -74,24 +78,6 @@ class Hyperopt(Backtesting):
|
||||||
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
|
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
|
||||||
return arg_dict
|
return arg_dict
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def save_trials(self) -> None:
|
def save_trials(self) -> None:
|
||||||
"""
|
"""
|
||||||
Save hyperopt trials to file
|
Save hyperopt trials to file
|
||||||
|
@ -121,7 +107,8 @@ class Hyperopt(Backtesting):
|
||||||
best_result['params']
|
best_result['params']
|
||||||
)
|
)
|
||||||
if 'roi_t1' in best_result['params']:
|
if 'roi_t1' in best_result['params']:
|
||||||
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
|
logger.info('ROI table:\n%s',
|
||||||
|
self.custom_hyperopt.generate_roi_table(best_result['params']))
|
||||||
|
|
||||||
def log_results(self, results) -> None:
|
def log_results(self, results) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -149,59 +136,6 @@ class Hyperopt(Backtesting):
|
||||||
result = trade_loss + profit_loss + duration_loss
|
result = trade_loss + profit_loss + duration_loss
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
|
||||||
"""
|
|
||||||
Generate the ROI table that will be used by Hyperopt
|
|
||||||
"""
|
|
||||||
roi_table = {}
|
|
||||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
|
||||||
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
|
||||||
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
|
||||||
|
|
||||||
return roi_table
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def roi_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Values to search for each ROI steps
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 120, name='roi_t1'),
|
|
||||||
Integer(10, 60, name='roi_t2'),
|
|
||||||
Integer(10, 40, name='roi_t3'),
|
|
||||||
Real(0.01, 0.04, name='roi_p1'),
|
|
||||||
Real(0.01, 0.07, name='roi_p2'),
|
|
||||||
Real(0.01, 0.20, name='roi_p3'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def stoploss_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Stoploss search space
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Real(-0.5, -0.02, name='stoploss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def indicator_space() -> List[Dimension]:
|
|
||||||
"""
|
|
||||||
Define your Hyperopt space for searching strategy parameters
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Integer(10, 25, name='mfi-value'),
|
|
||||||
Integer(15, 45, name='fastd-value'),
|
|
||||||
Integer(20, 50, name='adx-value'),
|
|
||||||
Integer(20, 40, name='rsi-value'),
|
|
||||||
Categorical([True, False], name='mfi-enabled'),
|
|
||||||
Categorical([True, False], name='fastd-enabled'),
|
|
||||||
Categorical([True, False], name='adx-enabled'),
|
|
||||||
Categorical([True, False], name='rsi-enabled'),
|
|
||||||
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
|
||||||
]
|
|
||||||
|
|
||||||
def has_space(self, space: str) -> bool:
|
def has_space(self, space: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Tell if a space value is contained in the configuration
|
Tell if a space value is contained in the configuration
|
||||||
|
@ -216,71 +150,33 @@ class Hyperopt(Backtesting):
|
||||||
"""
|
"""
|
||||||
spaces: List[Dimension] = []
|
spaces: List[Dimension] = []
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
spaces += Hyperopt.indicator_space()
|
spaces += self.custom_hyperopt.indicator_space()
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
spaces += Hyperopt.roi_space()
|
spaces += self.custom_hyperopt.roi_space()
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
spaces += Hyperopt.stoploss_space()
|
spaces += self.custom_hyperopt.stoploss_space()
|
||||||
return spaces
|
return spaces
|
||||||
|
|
||||||
@staticmethod
|
def generate_optimizer(self, _params: Dict) -> Dict:
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
|
||||||
"""
|
|
||||||
Define the buy strategy parameters to be used by hyperopt
|
|
||||||
"""
|
|
||||||
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Buy strategy Hyperopt will build and use
|
|
||||||
"""
|
|
||||||
conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if 'mfi-enabled' in params and params['mfi-enabled']:
|
|
||||||
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
|
||||||
if 'fastd-enabled' in params and params['fastd-enabled']:
|
|
||||||
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
|
||||||
if 'adx-enabled' in params and params['adx-enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx-value'])
|
|
||||||
if 'rsi-enabled' in params and params['rsi-enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
if params['trigger'] == 'bb_lower':
|
|
||||||
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
|
||||||
if params['trigger'] == 'macd_cross_signal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['macd'], dataframe['macdsignal']
|
|
||||||
))
|
|
||||||
if params['trigger'] == 'sar_reversal':
|
|
||||||
conditions.append(qtpylib.crossed_above(
|
|
||||||
dataframe['close'], dataframe['sar']
|
|
||||||
))
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
def generate_optimizer(self, _params) -> Dict:
|
|
||||||
params = self.get_args(_params)
|
params = self.get_args(_params)
|
||||||
|
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
self.strategy.minimal_roi = self.generate_roi_table(params)
|
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.advise_buy = self.buy_strategy_generator(params)
|
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
|
||||||
|
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
self.strategy.stoploss = params['stoploss']
|
self.strategy.stoploss = params['stoploss']
|
||||||
|
|
||||||
processed = load(TICKERDATA_PICKLE)
|
processed = load(TICKERDATA_PICKLE)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config['stake_amount'],
|
'stake_amount': self.config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'position_stacking': self.config.get('position_stacking', True),
|
'position_stacking': self.config.get('position_stacking', True),
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result_explanation = self.format_results(results)
|
result_explanation = self.format_results(results)
|
||||||
|
@ -329,7 +225,8 @@ class Hyperopt(Backtesting):
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_optimizer_parallel(self, parallel, asked) -> List:
|
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||||
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
|
return parallel(delayed(
|
||||||
|
wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked)
|
||||||
|
|
||||||
def load_previous_results(self):
|
def load_previous_results(self):
|
||||||
""" read trials file if we have one """
|
""" read trials file if we have one """
|
||||||
|
@ -344,15 +241,16 @@ class Hyperopt(Backtesting):
|
||||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
'timerange') is None else str(self.config.get('timerange')))
|
'timerange') is None else str(self.config.get('timerange')))
|
||||||
data = load_data(
|
data = load_data(
|
||||||
datadir=str(self.config.get('datadir')),
|
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
|
||||||
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.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
|
self.strategy.advise_indicators = \
|
||||||
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
self.custom_hyperopt.populate_indicators # type: ignore
|
||||||
|
dump(self.strategy.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
||||||
self.exchange = None # type: ignore
|
self.exchange = None # type: ignore
|
||||||
self.load_previous_results()
|
self.load_previous_results()
|
||||||
|
|
||||||
|
|
66
freqtrade/optimize/hyperopt_interface.py
Normal file
66
freqtrade/optimize/hyperopt_interface.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
"""
|
||||||
|
IHyperOpt interface
|
||||||
|
This module defines the interface to apply for hyperopts
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, Any, Callable, List
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
from skopt.space import Dimension
|
||||||
|
|
||||||
|
|
||||||
|
class IHyperOpt(ABC):
|
||||||
|
"""
|
||||||
|
Interface for freqtrade hyperopts
|
||||||
|
Defines the mandatory structure must follow any custom strategies
|
||||||
|
|
||||||
|
Attributes you can use:
|
||||||
|
minimal_roi -> Dict: Minimal ROI 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
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Populate indicators that will be used in the Buy and Sell strategy
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Create a buy strategy generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def indicator_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create an indicator space
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
|
"""
|
||||||
|
Create an roi table
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def stoploss_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create a stoploss space
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def roi_space() -> List[Dimension]:
|
||||||
|
"""
|
||||||
|
Create a roi space
|
||||||
|
"""
|
91
freqtrade/pairlist/IPairList.py
Normal file
91
freqtrade/pairlist/IPairList.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
"""
|
||||||
|
Static List provider
|
||||||
|
|
||||||
|
Provides lists as configured in config.json
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IPairList(ABC):
|
||||||
|
|
||||||
|
def __init__(self, freqtrade, config: dict) -> None:
|
||||||
|
self._freqtrade = freqtrade
|
||||||
|
self._config = config
|
||||||
|
self._whitelist = self._config['exchange']['pair_whitelist']
|
||||||
|
self._blacklist = self._config['exchange'].get('pair_blacklist', [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""
|
||||||
|
Gets name of the class
|
||||||
|
-> no need to overwrite in subclasses
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def whitelist(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Has the current whitelist
|
||||||
|
-> no need to overwrite in subclasses
|
||||||
|
"""
|
||||||
|
return self._whitelist
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blacklist(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Has the current blacklist
|
||||||
|
-> no need to overwrite in subclasses
|
||||||
|
"""
|
||||||
|
return self._blacklist
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
-> Please overwrite in subclasses
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def refresh_pairlist(self) -> None:
|
||||||
|
"""
|
||||||
|
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively
|
||||||
|
-> Please overwrite in subclasses
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _validate_whitelist(self, whitelist: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
trade
|
||||||
|
:return: the list of pairs the user wants to trade without the one unavailable or
|
||||||
|
black_listed
|
||||||
|
"""
|
||||||
|
sanitized_whitelist = whitelist
|
||||||
|
markets = self._freqtrade.exchange.get_markets()
|
||||||
|
|
||||||
|
# Filter to markets in stake currency
|
||||||
|
markets = [m for m in markets if m['quote'] == self._config['stake_currency']]
|
||||||
|
known_pairs = set()
|
||||||
|
|
||||||
|
for market in markets:
|
||||||
|
pair = market['symbol']
|
||||||
|
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
||||||
|
if pair not in whitelist or pair in self.blacklist:
|
||||||
|
continue
|
||||||
|
# else the pair is valid
|
||||||
|
known_pairs.add(pair)
|
||||||
|
# Market is not active
|
||||||
|
if not market['active']:
|
||||||
|
sanitized_whitelist.remove(pair)
|
||||||
|
logger.info(
|
||||||
|
'Ignoring %s from whitelist. Market is not active.',
|
||||||
|
pair
|
||||||
|
)
|
||||||
|
|
||||||
|
# We need to remove pairs that are unknown
|
||||||
|
return [x for x in sanitized_whitelist if x in known_pairs]
|
30
freqtrade/pairlist/StaticPairList.py
Normal file
30
freqtrade/pairlist/StaticPairList.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""
|
||||||
|
Static List provider
|
||||||
|
|
||||||
|
Provides lists as configured in config.json
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticPairList(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, freqtrade, config: dict) -> None:
|
||||||
|
super().__init__(freqtrade, config)
|
||||||
|
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
-> Please overwrite in subclasses
|
||||||
|
"""
|
||||||
|
return f"{self.name}: {self.whitelist}"
|
||||||
|
|
||||||
|
def refresh_pairlist(self) -> None:
|
||||||
|
"""
|
||||||
|
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively
|
||||||
|
"""
|
||||||
|
self._whitelist = self._validate_whitelist(self._config['exchange']['pair_whitelist'])
|
75
freqtrade/pairlist/VolumePairList.py
Normal file
75
freqtrade/pairlist/VolumePairList.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
"""
|
||||||
|
Static List provider
|
||||||
|
|
||||||
|
Provides lists as configured in config.json
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
||||||
|
|
||||||
|
|
||||||
|
class VolumePairList(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, freqtrade, config: dict) -> None:
|
||||||
|
super().__init__(freqtrade, config)
|
||||||
|
self._whitelistconf = self._config.get('pairlist', {}).get('config')
|
||||||
|
if 'number_assets' not in self._whitelistconf:
|
||||||
|
raise OperationalException(
|
||||||
|
f'`number_assets` not specified. Please check your configuration '
|
||||||
|
'for "pairlist.config.number_assets"')
|
||||||
|
self._number_pairs = self._whitelistconf['number_assets']
|
||||||
|
self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume')
|
||||||
|
|
||||||
|
if not self._freqtrade.exchange.exchange_has('fetchTickers'):
|
||||||
|
raise OperationalException(
|
||||||
|
'Exchange does not support dynamic whitelist.'
|
||||||
|
'Please edit your config and restart the bot'
|
||||||
|
)
|
||||||
|
if not self._validate_keys(self._sort_key):
|
||||||
|
raise OperationalException(
|
||||||
|
f'key {self._sort_key} not in {SORT_VALUES}')
|
||||||
|
|
||||||
|
def _validate_keys(self, key):
|
||||||
|
return key in SORT_VALUES
|
||||||
|
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
-> Please overwrite in subclasses
|
||||||
|
"""
|
||||||
|
return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs."
|
||||||
|
|
||||||
|
def refresh_pairlist(self) -> None:
|
||||||
|
"""
|
||||||
|
Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively
|
||||||
|
-> Please overwrite in subclasses
|
||||||
|
"""
|
||||||
|
# Generate dynamic whitelist
|
||||||
|
pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key)
|
||||||
|
# Validate whitelist to only have active market pairs
|
||||||
|
self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs]
|
||||||
|
|
||||||
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
|
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Updates the whitelist with with a dynamically generated list
|
||||||
|
:param base_currency: base currency as str
|
||||||
|
:param key: sort key (defaults to 'quoteVolume')
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
|
||||||
|
tickers = self._freqtrade.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
|
0
freqtrade/pairlist/__init__.py
Normal file
0
freqtrade/pairlist/__init__.py
Normal file
|
@ -4,7 +4,7 @@ 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
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
@ -14,6 +14,7 @@ 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 import func
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
@ -82,7 +83,7 @@ def check_migrate(engine) -> None:
|
||||||
logger.debug(f'trying {table_back_name}')
|
logger.debug(f'trying {table_back_name}')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'ticker_interval'):
|
if not has_column(cols, 'stoploss_order_id'):
|
||||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||||
|
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
|
@ -91,6 +92,7 @@ def check_migrate(engine) -> None:
|
||||||
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
||||||
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||||
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||||
|
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
|
||||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
strategy = get_column_def(cols, 'strategy', 'null')
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
|
@ -98,6 +100,9 @@ def check_migrate(engine) -> None:
|
||||||
|
|
||||||
# Schema migration necessary
|
# Schema migration necessary
|
||||||
engine.execute(f"alter table trades rename to {table_back_name}")
|
engine.execute(f"alter table trades rename to {table_back_name}")
|
||||||
|
# drop indexes on backup table
|
||||||
|
for index in inspector.get_indexes(table_back_name):
|
||||||
|
engine.execute(f"drop index {index['name']}")
|
||||||
# let SQLAlchemy create the schema as required
|
# let SQLAlchemy create the schema as required
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
|
||||||
|
@ -106,7 +111,7 @@ def check_migrate(engine) -> None:
|
||||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id,
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
stop_loss, initial_stop_loss, max_rate, sell_reason, strategy,
|
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy,
|
||||||
ticker_interval
|
ticker_interval
|
||||||
)
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
|
@ -122,7 +127,8 @@ def check_migrate(engine) -> None:
|
||||||
{close_rate_requested} close_rate_requested, close_profit,
|
{close_rate_requested} close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id,
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||||
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
|
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate,
|
||||||
|
{sell_reason} sell_reason, {strategy} strategy,
|
||||||
{ticker_interval} ticker_interval
|
{ticker_interval} ticker_interval
|
||||||
from {table_back_name}
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
|
@ -177,6 +183,8 @@ class Trade(_DECL_BASE):
|
||||||
stop_loss = Column(Float, nullable=True, default=0.0)
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
# absolute value of the initial stop loss
|
# absolute value of the initial stop loss
|
||||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# stoploss order id which is on exchange
|
||||||
|
stoploss_order_id = Column(String, nullable=True, index=True)
|
||||||
# absolute value of the highest reached price
|
# absolute value of the highest reached price
|
||||||
max_rate = Column(Float, nullable=True, default=0.0)
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
sell_reason = Column(String, nullable=True)
|
sell_reason = Column(String, nullable=True)
|
||||||
|
@ -239,17 +247,21 @@ class Trade(_DECL_BASE):
|
||||||
if order['status'] == 'open' or order['price'] 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=%s) ...', self.id)
|
||||||
|
|
||||||
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
if order_type in ('market', 'limit') and order['side'] == '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['price'])
|
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('%s_BUY has been fulfilled for %s.', order_type.upper(), self)
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
elif order_type == 'limit' and order['side'] == 'sell':
|
elif order_type in ('market', 'limit') and order['side'] == 'sell':
|
||||||
self.close(order['price'])
|
self.close(order['price'])
|
||||||
|
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
|
||||||
|
elif order_type == 'stop_loss_limit':
|
||||||
|
self.stoploss_order_id = None
|
||||||
|
logger.info('STOP_LOSS_LIMIT is hit for %s.', self)
|
||||||
|
self.close(order['average'])
|
||||||
else:
|
else:
|
||||||
raise ValueError(f'Unknown order type: {order_type}')
|
raise ValueError(f'Unknown order type: {order_type}')
|
||||||
cleanup()
|
cleanup()
|
||||||
|
@ -273,12 +285,11 @@ class Trade(_DECL_BASE):
|
||||||
self,
|
self,
|
||||||
fee: Optional[float] = None) -> float:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the open_rate in BTC
|
Calculate the open_rate including fee.
|
||||||
:param fee: fee to use on the open rate (optional).
|
:param fee: fee to use on the open rate (optional).
|
||||||
If rate is not set self.fee will be used
|
If rate is not set self.fee will be used
|
||||||
:return: Price in BTC of the open trade
|
:return: Price in of the open trade incl. Fees
|
||||||
"""
|
"""
|
||||||
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_open)
|
fees = buy_trade * Decimal(fee or self.fee_open)
|
||||||
|
@ -289,14 +300,13 @@ class Trade(_DECL_BASE):
|
||||||
rate: Optional[float] = None,
|
rate: Optional[float] = None,
|
||||||
fee: Optional[float] = None) -> float:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the close_rate in BTC
|
Calculate the close_rate including fee
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param fee: fee to use on the close rate (optional).
|
||||||
If rate is not set self.fee will be used
|
If rate is not set self.fee will be used
|
||||||
:param rate: rate to compare with (optional).
|
:param rate: rate to compare with (optional).
|
||||||
If rate is not set self.close_rate will be used
|
If rate is not set self.close_rate will be used
|
||||||
:return: Price in BTC of the open trade
|
:return: Price in BTC of the open trade
|
||||||
"""
|
"""
|
||||||
getcontext().prec = 8
|
|
||||||
|
|
||||||
if rate is None and not self.close_rate:
|
if rate is None and not self.close_rate:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
@ -310,12 +320,12 @@ class Trade(_DECL_BASE):
|
||||||
rate: Optional[float] = None,
|
rate: Optional[float] = None,
|
||||||
fee: Optional[float] = None) -> float:
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the profit in BTC between Close and Open trade
|
Calculate the absolute profit in stake currency between Close and Open trade
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param fee: fee to use on the close rate (optional).
|
||||||
If rate is not set self.fee will be used
|
If rate is not set self.fee will be used
|
||||||
:param rate: close rate to compare with (optional).
|
:param rate: close rate to compare with (optional).
|
||||||
If rate is not set self.close_rate will be used
|
If rate is not set self.close_rate will be used
|
||||||
:return: profit in BTC as float
|
:return: profit in stake currency as float
|
||||||
"""
|
"""
|
||||||
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(
|
||||||
|
@ -336,7 +346,6 @@ class Trade(_DECL_BASE):
|
||||||
:param fee: fee to use on the close rate (optional).
|
:param fee: fee to use on the close rate (optional).
|
||||||
:return: profit in percentage as float
|
:return: profit in percentage as float
|
||||||
"""
|
"""
|
||||||
getcontext().prec = 8
|
|
||||||
|
|
||||||
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(
|
||||||
|
@ -345,3 +354,14 @@ class Trade(_DECL_BASE):
|
||||||
)
|
)
|
||||||
profit_percent = (close_trade_price / open_trade_price) - 1
|
profit_percent = (close_trade_price / open_trade_price) - 1
|
||||||
return float(f"{profit_percent:.8f}")
|
return float(f"{profit_percent:.8f}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def total_open_trades_stakes() -> float:
|
||||||
|
"""
|
||||||
|
Calculates total invested amount in open trades
|
||||||
|
in stake currency
|
||||||
|
"""
|
||||||
|
total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\
|
||||||
|
.filter(Trade.is_open.is_(True))\
|
||||||
|
.scalar()
|
||||||
|
return total_open_stake_amount or 0
|
||||||
|
|
4
freqtrade/resolvers/__init__.py
Normal file
4
freqtrade/resolvers/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from freqtrade.resolvers.iresolver import IResolver # noqa: F401
|
||||||
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401
|
||||||
|
from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401
|
||||||
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401
|
67
freqtrade/resolvers/hyperopt_resolver.py
Normal file
67
freqtrade/resolvers/hyperopt_resolver.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# pragma pylint: disable=attribute-defined-outside-init
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module load custom hyperopts
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
from freqtrade.constants import DEFAULT_HYPEROPT
|
||||||
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
|
from freqtrade.resolvers import IResolver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HyperOptResolver(IResolver):
|
||||||
|
"""
|
||||||
|
This class contains all the logic to load custom hyperopt class
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ['hyperopt']
|
||||||
|
|
||||||
|
def __init__(self, config: Optional[Dict] = None) -> None:
|
||||||
|
"""
|
||||||
|
Load the custom class from config parameter
|
||||||
|
:param config: configuration dictionary or None
|
||||||
|
"""
|
||||||
|
config = config or {}
|
||||||
|
|
||||||
|
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
|
||||||
|
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
|
||||||
|
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
|
def _load_hyperopt(
|
||||||
|
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt:
|
||||||
|
"""
|
||||||
|
Search and loads the specified hyperopt.
|
||||||
|
:param hyperopt_name: name of the module to import
|
||||||
|
:param extra_dir: additional directory to search for the given hyperopt
|
||||||
|
:return: HyperOpt instance or None
|
||||||
|
"""
|
||||||
|
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||||
|
|
||||||
|
abs_paths = [
|
||||||
|
current_path.parent.parent.joinpath('user_data/hyperopts'),
|
||||||
|
current_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
if extra_dir:
|
||||||
|
# Add extra hyperopt directory on top of search paths
|
||||||
|
abs_paths.insert(0, Path(extra_dir))
|
||||||
|
|
||||||
|
for _path in abs_paths:
|
||||||
|
try:
|
||||||
|
hyperopt = self._search_object(directory=_path, object_type=IHyperOpt,
|
||||||
|
object_name=hyperopt_name)
|
||||||
|
if hyperopt:
|
||||||
|
logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, _path)
|
||||||
|
return hyperopt
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||||
|
|
||||||
|
raise ImportError(
|
||||||
|
"Impossible to load Hyperopt '{}'. This class does not exist"
|
||||||
|
" or contains Python code errors".format(hyperopt_name)
|
||||||
|
)
|
61
freqtrade/resolvers/iresolver.py
Normal file
61
freqtrade/resolvers/iresolver.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# pragma pylint: disable=attribute-defined-outside-init
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module load custom objects
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Type, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IResolver(object):
|
||||||
|
"""
|
||||||
|
This class contains all the logic to load custom classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_valid_object(object_type, module_path: Path,
|
||||||
|
object_name: str) -> Optional[Type[Any]]:
|
||||||
|
"""
|
||||||
|
Returns the first object with matching object_type and object_name in the path given.
|
||||||
|
:param object_type: object_type (class)
|
||||||
|
:param module_path: absolute path to the module
|
||||||
|
:param object_name: Class name of the object
|
||||||
|
:return: class or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generate spec based on absolute path
|
||||||
|
spec = importlib.util.spec_from_file_location('unknown', str(module_path))
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||||
|
|
||||||
|
valid_objects_gen = (
|
||||||
|
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||||
|
if object_name == name and object_type in obj.__bases__
|
||||||
|
)
|
||||||
|
return next(valid_objects_gen, None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _search_object(directory: Path, object_type, object_name: str,
|
||||||
|
kwargs: dict = {}) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Search for the objectname in the given directory
|
||||||
|
:param directory: relative or absolute directory path
|
||||||
|
:return: object instance
|
||||||
|
"""
|
||||||
|
logger.debug('Searching for %s %s in \'%s\'', object_type.__name__, object_name, directory)
|
||||||
|
for entry in directory.iterdir():
|
||||||
|
# Only consider python files
|
||||||
|
if not str(entry).endswith('.py'):
|
||||||
|
logger.debug('Ignoring %s', entry)
|
||||||
|
continue
|
||||||
|
obj = IResolver._get_valid_object(
|
||||||
|
object_type, Path.resolve(directory.joinpath(entry)), object_name
|
||||||
|
)
|
||||||
|
if obj:
|
||||||
|
return obj(**kwargs)
|
||||||
|
return None
|
59
freqtrade/resolvers/pairlist_resolver.py
Normal file
59
freqtrade/resolvers/pairlist_resolver.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# pragma pylint: disable=attribute-defined-outside-init
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module load custom hyperopts
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
from freqtrade.resolvers import IResolver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PairListResolver(IResolver):
|
||||||
|
"""
|
||||||
|
This class contains all the logic to load custom hyperopt class
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ['pairlist']
|
||||||
|
|
||||||
|
def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Load the custom class from config parameter
|
||||||
|
:param config: configuration dictionary or None
|
||||||
|
"""
|
||||||
|
self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade,
|
||||||
|
'config': config})
|
||||||
|
|
||||||
|
def _load_pairlist(
|
||||||
|
self, pairlist_name: str, kwargs: dict) -> IPairList:
|
||||||
|
"""
|
||||||
|
Search and loads the specified pairlist.
|
||||||
|
:param pairlist_name: name of the module to import
|
||||||
|
:param extra_dir: additional directory to search for the given pairlist
|
||||||
|
:return: PairList instance or None
|
||||||
|
"""
|
||||||
|
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
|
||||||
|
|
||||||
|
abs_paths = [
|
||||||
|
current_path.parent.parent.joinpath('user_data/pairlist'),
|
||||||
|
current_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
for _path in abs_paths:
|
||||||
|
try:
|
||||||
|
pairlist = self._search_object(directory=_path, object_type=IPairList,
|
||||||
|
object_name=pairlist_name,
|
||||||
|
kwargs=kwargs)
|
||||||
|
if pairlist:
|
||||||
|
logger.info('Using resolved pairlist %s from \'%s\'', pairlist_name, _path)
|
||||||
|
return pairlist
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||||
|
|
||||||
|
raise ImportError(
|
||||||
|
"Impossible to load Pairlist '{}'. This class does not exist"
|
||||||
|
" or contains Python code errors".format(pairlist_name)
|
||||||
|
)
|
|
@ -3,24 +3,23 @@
|
||||||
"""
|
"""
|
||||||
This module load custom strategies
|
This module load custom strategies
|
||||||
"""
|
"""
|
||||||
import importlib.util
|
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from base64 import urlsafe_b64decode
|
from base64 import urlsafe_b64decode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, Type
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.resolvers import IResolver
|
||||||
from freqtrade.strategy import import_strategy
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StrategyResolver(object):
|
class StrategyResolver(IResolver):
|
||||||
"""
|
"""
|
||||||
This class contains all the logic to load custom strategy class
|
This class contains all the logic to load custom strategy class
|
||||||
"""
|
"""
|
||||||
|
@ -75,6 +74,32 @@ class StrategyResolver(object):
|
||||||
else:
|
else:
|
||||||
config['process_only_new_candles'] = self.strategy.process_only_new_candles
|
config['process_only_new_candles'] = self.strategy.process_only_new_candles
|
||||||
|
|
||||||
|
if 'order_types' in config:
|
||||||
|
self.strategy.order_types = config['order_types']
|
||||||
|
logger.info(
|
||||||
|
"Override strategy 'order_types' with value in config file: %s.",
|
||||||
|
config['order_types']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config['order_types'] = self.strategy.order_types
|
||||||
|
|
||||||
|
if 'order_time_in_force' in config:
|
||||||
|
self.strategy.order_time_in_force = config['order_time_in_force']
|
||||||
|
logger.info(
|
||||||
|
"Override strategy 'order_time_in_force' with value in config file: %s.",
|
||||||
|
config['order_time_in_force']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config['order_time_in_force'] = self.strategy.order_time_in_force
|
||||||
|
|
||||||
|
if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES):
|
||||||
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
|
f"Order-types mapping is incomplete.")
|
||||||
|
|
||||||
|
if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF):
|
||||||
|
raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. "
|
||||||
|
f"Order-time-in-force mapping is incomplete.")
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
{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(),
|
||||||
|
@ -90,15 +115,16 @@ class StrategyResolver(object):
|
||||||
:param extra_dir: additional directory to search for the given strategy
|
:param extra_dir: additional directory to search for the given strategy
|
||||||
:return: Strategy instance or None
|
:return: Strategy instance or None
|
||||||
"""
|
"""
|
||||||
current_path = os.path.dirname(os.path.realpath(__file__))
|
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||||
|
|
||||||
abs_paths = [
|
abs_paths = [
|
||||||
os.path.join(os.getcwd(), 'user_data', 'strategies'),
|
Path.cwd().joinpath('user_data/strategies'),
|
||||||
current_path,
|
current_path,
|
||||||
]
|
]
|
||||||
|
|
||||||
if extra_dir:
|
if extra_dir:
|
||||||
# Add extra strategy directory on top of search paths
|
# Add extra strategy directory on top of search paths
|
||||||
abs_paths.insert(0, extra_dir)
|
abs_paths.insert(0, Path(extra_dir).resolve())
|
||||||
|
|
||||||
if ":" in strategy_name:
|
if ":" in strategy_name:
|
||||||
logger.info("loading base64 endocded strategy")
|
logger.info("loading base64 endocded strategy")
|
||||||
|
@ -111,16 +137,17 @@ class StrategyResolver(object):
|
||||||
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
|
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
|
||||||
temp.joinpath("__init__.py").touch()
|
temp.joinpath("__init__.py").touch()
|
||||||
|
|
||||||
strategy_name = os.path.splitext(name)[0]
|
strategy_name = strat[0]
|
||||||
|
|
||||||
# register temp path with the bot
|
# register temp path with the bot
|
||||||
abs_paths.insert(0, str(temp.resolve()))
|
abs_paths.insert(0, temp.resolve())
|
||||||
|
|
||||||
for path in abs_paths:
|
for _path in abs_paths:
|
||||||
try:
|
try:
|
||||||
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
|
strategy = self._search_object(directory=_path, object_type=IStrategy,
|
||||||
|
object_name=strategy_name, kwargs={'config': config})
|
||||||
if strategy:
|
if strategy:
|
||||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, _path)
|
||||||
strategy._populate_fun_len = len(
|
strategy._populate_fun_len = len(
|
||||||
inspect.getfullargspec(strategy.populate_indicators).args)
|
inspect.getfullargspec(strategy.populate_indicators).args)
|
||||||
strategy._buy_fun_len = len(
|
strategy._buy_fun_len = len(
|
||||||
|
@ -130,49 +157,9 @@ class StrategyResolver(object):
|
||||||
|
|
||||||
return import_strategy(strategy, config=config)
|
return import_strategy(strategy, config=config)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning('Path "%s" does not exist', path)
|
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
|
||||||
|
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Impossible to load Strategy '{}'. This class does not exist"
|
"Impossible to load Strategy '{}'. This class does not exist"
|
||||||
" or contains Python code errors".format(strategy_name)
|
" or contains Python code errors".format(strategy_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
|
|
||||||
"""
|
|
||||||
Returns a list of all possible strategies for the given module_path
|
|
||||||
:param module_path: absolute path to the module
|
|
||||||
:param strategy_name: Class name of the strategy
|
|
||||||
:return: Tuple with (name, class) or None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Generate spec based on absolute path
|
|
||||||
spec = importlib.util.spec_from_file_location('unknown', module_path)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
|
||||||
|
|
||||||
valid_strategies_gen = (
|
|
||||||
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
|
||||||
if strategy_name == name and IStrategy in obj.__bases__
|
|
||||||
)
|
|
||||||
return next(valid_strategies_gen, None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]:
|
|
||||||
"""
|
|
||||||
Search for the strategy_name in the given directory
|
|
||||||
:param directory: relative or absolute directory path
|
|
||||||
:return: name of the strategy class
|
|
||||||
"""
|
|
||||||
logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory)
|
|
||||||
for entry in os.listdir(directory):
|
|
||||||
# Only consider python files
|
|
||||||
if not entry.endswith('.py'):
|
|
||||||
logger.debug('Ignoring %s', entry)
|
|
||||||
continue
|
|
||||||
strategy = StrategyResolver._get_valid_strategies(
|
|
||||||
os.path.abspath(os.path.join(directory, entry)), strategy_name
|
|
||||||
)
|
|
||||||
if strategy:
|
|
||||||
return strategy(config)
|
|
||||||
return None
|
|
|
@ -10,13 +10,13 @@ from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from numpy import mean, nan_to_num
|
from numpy import mean, nan_to_num, NAN
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import TemporaryError
|
from freqtrade import TemporaryError, DependencyException
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
|
@ -84,9 +84,7 @@ class RPC(object):
|
||||||
"""
|
"""
|
||||||
# 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 not trades:
|
||||||
raise RPCException('trader is not running')
|
|
||||||
elif not trades:
|
|
||||||
raise RPCException('no active trade')
|
raise RPCException('no active trade')
|
||||||
else:
|
else:
|
||||||
results = []
|
results = []
|
||||||
|
@ -95,7 +93,10 @@ class RPC(object):
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
|
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
|
||||||
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
except DependencyException:
|
||||||
|
current_rate = NAN
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||||
if trade.close_profit else None)
|
if trade.close_profit else None)
|
||||||
|
@ -118,15 +119,16 @@ class RPC(object):
|
||||||
|
|
||||||
def _rpc_status_table(self) -> DataFrame:
|
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 not trades:
|
||||||
raise RPCException('trader is not running')
|
|
||||||
elif not trades:
|
|
||||||
raise RPCException('no active order')
|
raise RPCException('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
|
||||||
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
except DependencyException:
|
||||||
|
current_rate = NAN
|
||||||
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
||||||
trades_list.append([
|
trades_list.append([
|
||||||
trade.id,
|
trade.id,
|
||||||
|
@ -211,7 +213,10 @@ class RPC(object):
|
||||||
profit_closed_percent.append(profit_percent)
|
profit_closed_percent.append(profit_percent)
|
||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
|
try:
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
except DependencyException:
|
||||||
|
current_rate = NAN
|
||||||
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(
|
||||||
|
@ -279,7 +284,7 @@ class RPC(object):
|
||||||
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
||||||
else:
|
else:
|
||||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||||
except TemporaryError:
|
except (TemporaryError, DependencyException):
|
||||||
continue
|
continue
|
||||||
est_btc: float = rate * balance['total']
|
est_btc: float = rate * balance['total']
|
||||||
total = total + est_btc
|
total = total + est_btc
|
||||||
|
@ -363,6 +368,7 @@ class RPC(object):
|
||||||
# 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)
|
||||||
|
Trade.session.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
|
@ -379,13 +385,45 @@ class RPC(object):
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
||||||
|
"""
|
||||||
|
Handler for forcebuy <asset> <price>
|
||||||
|
Buys a pair trade at the given or current price
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self._freqtrade.config.get('forcebuy_enable', False):
|
||||||
|
raise RPCException('Forcebuy not enabled.')
|
||||||
|
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
|
# Check pair is in stake currency
|
||||||
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
|
if not pair.endswith(stake_currency):
|
||||||
|
raise RPCException(
|
||||||
|
f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only')
|
||||||
|
# check if valid pair
|
||||||
|
|
||||||
|
# check if pair already has an open pair
|
||||||
|
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
||||||
|
if trade:
|
||||||
|
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||||
|
|
||||||
|
# gen stake amount
|
||||||
|
stakeamount = self._freqtrade._get_trade_stake_amount(pair)
|
||||||
|
|
||||||
|
# execute buy
|
||||||
|
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
||||||
|
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
||||||
|
return trade
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def _rpc_performance(self) -> List[Dict]:
|
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:
|
|
||||||
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'),
|
||||||
|
@ -405,3 +443,11 @@ class RPC(object):
|
||||||
raise RPCException('trader is not running')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
def _rpc_whitelist(self) -> Dict:
|
||||||
|
""" Returns the currently active whitelist"""
|
||||||
|
res = {'method': self._freqtrade.pairlists.name,
|
||||||
|
'length': len(self._freqtrade.pairlists.whitelist),
|
||||||
|
'whitelist': self._freqtrade.active_pair_whitelist
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
|
@ -4,7 +4,7 @@ This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC, RPCMessageType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -51,3 +51,29 @@ class RPCManager(object):
|
||||||
for mod in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
logger.debug('Forwarding message to rpc.%s', mod.name)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
mod.send_msg(msg)
|
mod.send_msg(msg)
|
||||||
|
|
||||||
|
def startup_messages(self, config, pairlist) -> None:
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
|
'status': 'Dry run is enabled. All trades are simulated.'
|
||||||
|
})
|
||||||
|
stake_currency = config['stake_currency']
|
||||||
|
stake_amount = config['stake_amount']
|
||||||
|
minimal_roi = config['minimal_roi']
|
||||||
|
ticker_interval = config['ticker_interval']
|
||||||
|
exchange_name = config['exchange']['name']
|
||||||
|
strategy_name = config.get('strategy', '')
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||||
|
'status': f'*Exchange:* `{exchange_name}`\n'
|
||||||
|
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||||
|
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||||
|
f'*Ticker Interval:* `{ticker_interval}`\n'
|
||||||
|
f'*Strategy:* `{strategy_name}`'
|
||||||
|
})
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Searching for {stake_currency} pairs to buy and sell '
|
||||||
|
f'based on {pairlist.short_desc()}'
|
||||||
|
})
|
||||||
|
|
|
@ -12,8 +12,8 @@ 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.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||||
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -86,10 +86,12 @@ class Telegram(RPC):
|
||||||
CommandHandler('start', self._start),
|
CommandHandler('start', self._start),
|
||||||
CommandHandler('stop', self._stop),
|
CommandHandler('stop', self._stop),
|
||||||
CommandHandler('forcesell', self._forcesell),
|
CommandHandler('forcesell', self._forcesell),
|
||||||
|
CommandHandler('forcebuy', self._forcebuy),
|
||||||
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('reload_conf', self._reload_conf),
|
||||||
|
CommandHandler('whitelist', self._whitelist),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
CommandHandler('version', self._version),
|
||||||
]
|
]
|
||||||
|
@ -123,9 +125,9 @@ class Telegram(RPC):
|
||||||
else:
|
else:
|
||||||
msg['stake_amount_fiat'] = 0
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
message = "*{exchange}:* Buying [{pair}]({market_url})\n" \
|
message = ("*{exchange}:* Buying [{pair}]({market_url})\n"
|
||||||
"with limit `{limit:.8f}\n" \
|
"with limit `{limit:.8f}\n"
|
||||||
"({stake_amount:.6f} {stake_currency}".format(**msg)
|
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||||
|
|
||||||
if msg.get('fiat_currency', None):
|
if msg.get('fiat_currency', None):
|
||||||
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||||
|
@ -135,12 +137,13 @@ class Telegram(RPC):
|
||||||
msg['amount'] = round(msg['amount'], 8)
|
msg['amount'] = round(msg['amount'], 8)
|
||||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||||
|
|
||||||
message = "*{exchange}:* Selling [{pair}]({market_url})\n" \
|
message = ("*{exchange}:* Selling [{pair}]({market_url})\n"
|
||||||
"*Limit:* `{limit:.8f}`\n" \
|
"*Limit:* `{limit:.8f}`\n"
|
||||||
"*Amount:* `{amount:.8f}`\n" \
|
"*Amount:* `{amount:.8f}`\n"
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||||
"*Profit:* `{profit_percent:.2f}%`".format(**msg)
|
"*Sell Reason:* `{sell_reason}`\n"
|
||||||
|
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
||||||
|
|
||||||
# Check if all sell properties are available.
|
# Check if all sell properties are available.
|
||||||
# This might not be the case if the message origin is triggered by /forcesell
|
# This might not be the case if the message origin is triggered by /forcesell
|
||||||
|
@ -148,8 +151,8 @@ class Telegram(RPC):
|
||||||
and self._fiat_converter):
|
and self._fiat_converter):
|
||||||
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \
|
message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`'
|
||||||
'` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg)
|
'` / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||||
|
|
||||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||||
message = '*Status:* `{status}`'.format(**msg)
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
@ -307,11 +310,14 @@ class Telegram(RPC):
|
||||||
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||||
output = ''
|
output = ''
|
||||||
for currency in result['currencies']:
|
for currency in result['currencies']:
|
||||||
|
if currency['est_btc'] > 0.0001:
|
||||||
output += "*{currency}:*\n" \
|
output += "*{currency}:*\n" \
|
||||||
"\t`Available: {available: .8f}`\n" \
|
"\t`Available: {available: .8f}`\n" \
|
||||||
"\t`Balance: {balance: .8f}`\n" \
|
"\t`Balance: {balance: .8f}`\n" \
|
||||||
"\t`Pending: {pending: .8f}`\n" \
|
"\t`Pending: {pending: .8f}`\n" \
|
||||||
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||||
|
else:
|
||||||
|
output += "*{currency}:* not showing <1$ amount \n".format(**currency)
|
||||||
|
|
||||||
output += "\n*Estimated Value*:\n" \
|
output += "\n*Estimated Value*:\n" \
|
||||||
"\t`BTC: {total: .8f}`\n" \
|
"\t`BTC: {total: .8f}`\n" \
|
||||||
|
@ -372,6 +378,24 @@ class Telegram(RPC):
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _forcebuy(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /forcebuy <asset> <price>.
|
||||||
|
Buys a pair trade at the given or current price
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
message = update.message.text.replace('/forcebuy', '').strip().split()
|
||||||
|
pair = message[0]
|
||||||
|
price = float(message[1]) if len(message) > 1 else None
|
||||||
|
try:
|
||||||
|
self._rpc_forcebuy(pair, price)
|
||||||
|
except RPCException as e:
|
||||||
|
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:
|
||||||
"""
|
"""
|
||||||
|
@ -416,6 +440,23 @@ class Telegram(RPC):
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _whitelist(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /whitelist
|
||||||
|
Shows the currently active whitelist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
whitelist = self._rpc_whitelist()
|
||||||
|
|
||||||
|
message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n"
|
||||||
|
message += f"`{', '.join(whitelist['whitelist'])}`"
|
||||||
|
|
||||||
|
logger.debug(message)
|
||||||
|
self._send_msg(message)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _help(self, bot: Bot, update: Update) -> None:
|
def _help(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -437,6 +478,8 @@ class Telegram(RPC):
|
||||||
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
"*/count:* `Show number of trades running compared to allowed number of trades`" \
|
||||||
"\n" \
|
"\n" \
|
||||||
"*/balance:* `Show account balance per currency`\n" \
|
"*/balance:* `Show account balance per currency`\n" \
|
||||||
|
"*/reload_conf:* `Reload configuration file` \n" \
|
||||||
|
"*/whitelist:* `Show current whitelist` \n" \
|
||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n" \
|
||||||
"*/version:* `Show version`"
|
"*/version:* `Show version`"
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,20 @@ class DefaultStrategy(IStrategy):
|
||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = '5m'
|
ticker_interval = '5m'
|
||||||
|
|
||||||
|
# Optional order type mapping
|
||||||
|
order_types = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional time in force for orders
|
||||||
|
order_time_in_force = {
|
||||||
|
'buy': 'gtc',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
|
@ -6,14 +6,13 @@ import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
from typing import Dict, List, NamedTuple, Tuple
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -33,6 +32,7 @@ class SellType(Enum):
|
||||||
"""
|
"""
|
||||||
ROI = "roi"
|
ROI = "roi"
|
||||||
STOP_LOSS = "stop_loss"
|
STOP_LOSS = "stop_loss"
|
||||||
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
SELL_SIGNAL = "sell_signal"
|
SELL_SIGNAL = "sell_signal"
|
||||||
FORCE_SELL = "force_sell"
|
FORCE_SELL = "force_sell"
|
||||||
|
@ -70,6 +70,20 @@ class IStrategy(ABC):
|
||||||
# associated ticker interval
|
# associated ticker interval
|
||||||
ticker_interval: str
|
ticker_interval: str
|
||||||
|
|
||||||
|
# Optional order types
|
||||||
|
order_types: Dict = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional time in force
|
||||||
|
order_time_in_force: Dict = {
|
||||||
|
'buy': 'gtc',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
# run "populate_indicators" only for new candle
|
# run "populate_indicators" only for new candle
|
||||||
process_only_new_candles: bool = False
|
process_only_new_candles: bool = False
|
||||||
|
|
||||||
|
@ -113,19 +127,17 @@ class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Parses the given ticker history and returns a populated DataFrame
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
:return DataFrame with ticker data and indicator data
|
:return DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
|
||||||
|
|
||||||
pair = str(metadata.get('pair'))
|
pair = str(metadata.get('pair'))
|
||||||
|
|
||||||
# Test if seen this pair and last candle before.
|
# Test if seen this pair and last candle before.
|
||||||
# always run if process_only_new_candles is set to true
|
# always run if process_only_new_candles is set to false
|
||||||
if (not self.process_only_new_candles or
|
if (not self.process_only_new_candles or
|
||||||
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
||||||
# Defs that only make change on new candle data.
|
# Defs that only make change on new candle data.
|
||||||
|
@ -146,19 +158,20 @@ class IStrategy(ABC):
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def get_signal(self, pair: str, interval: str,
|
def get_signal(self, pair: str, interval: str,
|
||||||
ticker_hist: Optional[List[Dict]]) -> Tuple[bool, bool]:
|
dataframe: DataFrame) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Calculates current signal based several technical analysis indicators
|
Calculates current signal based several technical analysis indicators
|
||||||
:param pair: pair in format ANT/BTC
|
:param pair: pair in format ANT/BTC
|
||||||
:param interval: Interval to use (in min)
|
:param interval: Interval to use (in min)
|
||||||
|
:param dataframe: Dataframe to analyze
|
||||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||||
"""
|
"""
|
||||||
if not ticker_hist:
|
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
|
dataframe = self.analyze_ticker(dataframe, {'pair': pair})
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Unable to analyze ticker for pair %s: %s',
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
|
@ -203,18 +216,31 @@ class IStrategy(ABC):
|
||||||
return buy, sell
|
return buy, sell
|
||||||
|
|
||||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||||
sell: bool) -> SellCheckTuple:
|
sell: bool, low: float = None, high: float = None,
|
||||||
|
force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluate if on the condition required to trigger a sell has been reached
|
This function evaluate if on the condition required to trigger a sell has been reached
|
||||||
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)
|
|
||||||
stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
|
# Set current rate to low for backtesting sell
|
||||||
current_profit=current_profit)
|
current_rate = low or rate
|
||||||
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
|
||||||
|
if self.order_types.get('stoploss_on_exchange'):
|
||||||
|
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
else:
|
||||||
|
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||||
|
current_time=date, current_profit=current_profit,
|
||||||
|
force_stoploss=force_stoploss)
|
||||||
|
|
||||||
if stoplossflag.sell_flag:
|
if stoplossflag.sell_flag:
|
||||||
return stoplossflag
|
return stoplossflag
|
||||||
|
|
||||||
|
# Set current rate to low for backtesting sell
|
||||||
|
current_rate = high or rate
|
||||||
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
|
|
||||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||||
|
@ -237,7 +263,7 @@ class IStrategy(ABC):
|
||||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
|
||||||
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
|
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
|
||||||
current_profit: float) -> SellCheckTuple:
|
current_profit: float, force_stoploss: float) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
Based on current profit of the trade and configured (trailing) stoploss,
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
decides to sell or not
|
decides to sell or not
|
||||||
|
@ -246,7 +272,8 @@ class IStrategy(ABC):
|
||||||
|
|
||||||
trailing_stop = self.config.get('trailing_stop', False)
|
trailing_stop = self.config.get('trailing_stop', False)
|
||||||
|
|
||||||
trade.adjust_stop_loss(trade.open_rate, self.stoploss, initial=True)
|
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
|
||||||
|
else self.stoploss, initial=True)
|
||||||
|
|
||||||
# evaluate if the stoploss was hit
|
# evaluate if the stoploss was hit
|
||||||
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
||||||
|
@ -304,7 +331,7 @@ class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
Creates a dataframe and populates indicators for given ticker data
|
Creates a dataframe and populates indicators for given ticker data
|
||||||
"""
|
"""
|
||||||
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
|
return {pair: self.advise_indicators(pair_data, {'pair': pair})
|
||||||
for pair, pair_data in tickerdata.items()}
|
for pair, pair_data in tickerdata.items()}
|
||||||
|
|
||||||
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
|
@ -10,8 +10,10 @@ import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
from freqtrade import constants
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.edge import Edge, PairInfo
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
logging.getLogger('').setLevel(logging.INFO)
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
@ -25,24 +27,54 @@ def log_has(line, logs):
|
||||||
False)
|
False)
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None) -> None:
|
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex"))
|
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex"))
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||||
|
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
|
||||||
|
|
||||||
|
|
||||||
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
|
def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchange:
|
||||||
patch_exchange(mocker, api_mock)
|
patch_exchange(mocker, api_mock, id)
|
||||||
exchange = Exchange(config)
|
exchange = Exchange(config)
|
||||||
return exchange
|
return exchange
|
||||||
|
|
||||||
|
|
||||||
|
def patch_wallet(mocker, free=999.9) -> None:
|
||||||
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(
|
||||||
|
return_value=free
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def patch_edge(mocker) -> None:
|
||||||
|
# "ETH/BTC",
|
||||||
|
# "LTC/BTC",
|
||||||
|
# "XRP/BTC",
|
||||||
|
# "NEO/BTC"
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'NEO/BTC': PairInfo(-0.20, 0.66, 3.71, 0.50, 1.71, 10, 25),
|
||||||
|
'LTC/BTC': PairInfo(-0.21, 0.66, 3.71, 0.50, 1.71, 11, 20),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True))
|
||||||
|
|
||||||
|
|
||||||
|
def get_patched_edge(mocker, config) -> Edge:
|
||||||
|
patch_edge(mocker)
|
||||||
|
edge = Edge(config)
|
||||||
|
return edge
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
|
||||||
|
|
||||||
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
"""
|
"""
|
||||||
This function patch _init_modules() to not call dependencies
|
This function patch _init_modules() to not call dependencies
|
||||||
|
@ -50,7 +82,6 @@ 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})
|
|
||||||
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
||||||
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())
|
||||||
|
@ -75,7 +106,7 @@ def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> Non
|
||||||
'website_slug': 'ethereum'}
|
'website_slug': 'ethereum'}
|
||||||
]})
|
]})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
ticker=tickermock,
|
ticker=tickermock,
|
||||||
listings=listmock,
|
listings=listmock,
|
||||||
|
|
||||||
|
@ -356,6 +387,36 @@ def limit_buy_order():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def market_buy_order():
|
||||||
|
return {
|
||||||
|
'id': 'mocked_market_buy',
|
||||||
|
'type': 'market',
|
||||||
|
'side': 'buy',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'price': 0.00004099,
|
||||||
|
'amount': 91.99181073,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'status': 'closed'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def market_sell_order():
|
||||||
|
return {
|
||||||
|
'id': 'mocked_limit_sell',
|
||||||
|
'type': 'market',
|
||||||
|
'side': 'sell',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'datetime': arrow.utcnow().isoformat(),
|
||||||
|
'price': 0.00004173,
|
||||||
|
'amount': 91.99181073,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'status': 'closed'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def limit_buy_order_old():
|
def limit_buy_order_old():
|
||||||
return {
|
return {
|
||||||
|
@ -450,7 +511,7 @@ def order_book_l2():
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history():
|
def ticker_history_list():
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
1511686200000, # unix timestamp ms
|
1511686200000, # unix timestamp ms
|
||||||
|
@ -479,6 +540,11 @@ def ticker_history():
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ticker_history(ticker_history_list):
|
||||||
|
return parse_ticker_dataframe(ticker_history_list)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tickers():
|
def tickers():
|
||||||
return MagicMock(return_value={
|
return MagicMock(return_value={
|
||||||
|
@ -752,3 +818,26 @@ def buy_order_fee():
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'fee': None
|
'fee': None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def edge_conf(default_conf):
|
||||||
|
default_conf['max_open_trades'] = -1
|
||||||
|
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
|
default_conf['edge'] = {
|
||||||
|
"enabled": True,
|
||||||
|
"process_throttle_secs": 1800,
|
||||||
|
"calculate_since_number_of_days": 14,
|
||||||
|
"capital_available_percentage": 0.5,
|
||||||
|
"allowed_risk": 0.01,
|
||||||
|
"stoploss_range_min": -0.01,
|
||||||
|
"stoploss_range_max": -0.1,
|
||||||
|
"stoploss_range_step": -0.01,
|
||||||
|
"maximum_winrate": 0.80,
|
||||||
|
"minimum_expectancy": 0.20,
|
||||||
|
"min_trade_number": 15,
|
||||||
|
"max_trade_duration_minute": 1440,
|
||||||
|
"remove_pumps": False
|
||||||
|
}
|
||||||
|
|
||||||
|
return default_conf
|
||||||
|
|
0
freqtrade/tests/data/__init__.py
Normal file
0
freqtrade/tests/data/__init__.py
Normal file
|
@ -1,6 +1,8 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
import logging
|
||||||
|
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
|
@ -13,9 +15,11 @@ def test_dataframe_correct_columns(result):
|
||||||
['date', 'open', 'high', 'low', 'close', 'volume']
|
['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ticker_dataframe(ticker_history):
|
def test_parse_ticker_dataframe(ticker_history, caplog):
|
||||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
# Test file with BV data
|
# Test file with BV data
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
dataframe = parse_ticker_dataframe(ticker_history)
|
||||||
assert dataframe.columns.tolist() == columns
|
assert dataframe.columns.tolist() == columns
|
||||||
|
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples)
|
475
freqtrade/tests/data/test_history.py
Normal file
475
freqtrade/tests/data/test_history.py
Normal file
|
@ -0,0 +1,475 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
|
from freqtrade.data.history import (download_pair_history,
|
||||||
|
load_cached_data_for_updating,
|
||||||
|
load_tickerdata_file,
|
||||||
|
make_testdata_path,
|
||||||
|
trim_tickerlist)
|
||||||
|
from freqtrade.misc import file_dump_json
|
||||||
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
|
|
||||||
|
# Change this if modifying UNITTEST/BTC testdatafile
|
||||||
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_file(file: str, copy_file: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Backup existing file to avoid deleting the user file
|
||||||
|
:param file: complete path to the file
|
||||||
|
:param touch_file: create an empty file in replacement
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file_swp = file + '.swp'
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.rename(file, file_swp)
|
||||||
|
|
||||||
|
if copy_file:
|
||||||
|
copyfile(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_test_file(file: str) -> None:
|
||||||
|
"""
|
||||||
|
Backup existing file to avoid deleting the user file
|
||||||
|
:param file: complete path to the file
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file_swp = file + '.swp'
|
||||||
|
# 1. Delete file from the test
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
|
# 2. Rollback to the initial file
|
||||||
|
if os.path.isfile(file_swp):
|
||||||
|
os.rename(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None:
|
||||||
|
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='30m', datadir=None)
|
||||||
|
assert isinstance(ld, DataFrame)
|
||||||
|
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
|
||||||
|
ld = history.load_pair_history(pair='UNITTEST/BTC', ticker_interval='7m', datadir=None)
|
||||||
|
assert not isinstance(ld, DataFrame)
|
||||||
|
assert ld is None
|
||||||
|
assert log_has(
|
||||||
|
'No data for pair: "UNITTEST/BTC", Interval: 7m. '
|
||||||
|
'Use --refresh-pairs-cached to download the data', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
|
_backup_file(file, copy_file=True)
|
||||||
|
history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test load_pair_history() with 1 min ticker
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
|
_backup_file(file)
|
||||||
|
# do not download a new pair if refresh_pairs isn't set
|
||||||
|
history.load_pair_history(datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=False,
|
||||||
|
pair='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
|
||||||
|
history.load_pair_history(datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=True,
|
||||||
|
exchange=exchange,
|
||||||
|
pair='MEME/BTC')
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
|
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
|
||||||
|
history.load_pair_history(datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
refresh_pairs=True,
|
||||||
|
exchange=None,
|
||||||
|
pair='MEME/BTC')
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdata_path() -> None:
|
||||||
|
assert str(Path('freqtrade') / 'tests' / 'testdata') in str(make_testdata_path(None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_cached_data_for_updating(mocker) -> None:
|
||||||
|
datadir = Path(__file__).parent.parent.joinpath('testdata')
|
||||||
|
|
||||||
|
test_data = None
|
||||||
|
test_filename = datadir.joinpath('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.with_name('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.with_name('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.with_name('unexist'),
|
||||||
|
'1m',
|
||||||
|
None)
|
||||||
|
assert data == []
|
||||||
|
assert start_ts is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
|
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
|
||||||
|
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
|
||||||
|
|
||||||
|
_backup_file(file1_1)
|
||||||
|
_backup_file(file1_5)
|
||||||
|
_backup_file(file2_1)
|
||||||
|
_backup_file(file2_5)
|
||||||
|
|
||||||
|
assert os.path.isfile(file1_1) is False
|
||||||
|
assert os.path.isfile(file2_1) is False
|
||||||
|
|
||||||
|
assert download_pair_history(datadir=None, exchange=exchange,
|
||||||
|
pair='MEME/BTC',
|
||||||
|
tick_interval='1m')
|
||||||
|
assert download_pair_history(datadir=None, exchange=exchange,
|
||||||
|
pair='CFI/BTC',
|
||||||
|
tick_interval='1m')
|
||||||
|
assert not exchange._pairs_last_refresh_time
|
||||||
|
assert os.path.isfile(file1_1) is True
|
||||||
|
assert os.path.isfile(file2_1) is True
|
||||||
|
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_1)
|
||||||
|
_clean_test_file(file2_1)
|
||||||
|
|
||||||
|
assert os.path.isfile(file1_5) is False
|
||||||
|
assert os.path.isfile(file2_5) is False
|
||||||
|
|
||||||
|
assert download_pair_history(datadir=None, exchange=exchange,
|
||||||
|
pair='MEME/BTC',
|
||||||
|
tick_interval='5m')
|
||||||
|
assert download_pair_history(datadir=None, exchange=exchange,
|
||||||
|
pair='CFI/BTC',
|
||||||
|
tick_interval='5m')
|
||||||
|
assert not exchange._pairs_last_refresh_time
|
||||||
|
assert os.path.isfile(file1_5) is True
|
||||||
|
assert os.path.isfile(file2_5) is True
|
||||||
|
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_5)
|
||||||
|
_clean_test_file(file2_5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_pair_history2(mocker, default_conf) -> None:
|
||||||
|
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)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
|
||||||
|
download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
||||||
|
assert json_dump_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_history',
|
||||||
|
side_effect=BaseException('File Error'))
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
|
_backup_file(file1_1)
|
||||||
|
_backup_file(file1_5)
|
||||||
|
|
||||||
|
assert not download_pair_history(datadir=None, exchange=exchange,
|
||||||
|
pair='MEME/BTC',
|
||||||
|
tick_interval='1m')
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_1)
|
||||||
|
_clean_test_file(file1_5)
|
||||||
|
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_tickerdata_file() -> None:
|
||||||
|
# 7 does not exist in either format.
|
||||||
|
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
|
||||||
|
# 1 exists only as a .json
|
||||||
|
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
|
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
|
||||||
|
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
|
||||||
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_partial_missing(caplog) -> None:
|
||||||
|
# Make sure we start fresh - test missing data at start
|
||||||
|
start = arrow.get('2018-01-01T00:00:00')
|
||||||
|
end = arrow.get('2018-01-11T00:00:00')
|
||||||
|
tickerdata = history.load_data(None, '5m', ['UNITTEST/BTC'],
|
||||||
|
refresh_pairs=False,
|
||||||
|
timerange=TimeRange('date', 'date',
|
||||||
|
start.timestamp, end.timestamp))
|
||||||
|
# timedifference in 5 minutes
|
||||||
|
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||||
|
assert td != len(tickerdata['UNITTEST/BTC'])
|
||||||
|
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
|
||||||
|
assert log_has(f'Missing data at start for pair '
|
||||||
|
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||||
|
caplog.record_tuples)
|
||||||
|
# Make sure we start fresh - test missing data at end
|
||||||
|
caplog.clear()
|
||||||
|
start = arrow.get('2018-01-10T00:00:00')
|
||||||
|
end = arrow.get('2018-02-20T00:00:00')
|
||||||
|
tickerdata = history.load_data(datadir=None, ticker_interval='5m',
|
||||||
|
pairs=['UNITTEST/BTC'], refresh_pairs=False,
|
||||||
|
timerange=TimeRange('date', 'date',
|
||||||
|
start.timestamp, end.timestamp))
|
||||||
|
# timedifference in 5 minutes
|
||||||
|
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||||
|
assert td != len(tickerdata['UNITTEST/BTC'])
|
||||||
|
# Shift endtime with +5 - as last candle is dropped (partial candle)
|
||||||
|
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
|
||||||
|
assert log_has(f'Missing data at end for pair '
|
||||||
|
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init(default_conf, mocker) -> None:
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert {} == history.load_data(
|
||||||
|
datadir='',
|
||||||
|
exchange=exchange,
|
||||||
|
pairs=[],
|
||||||
|
refresh_pairs=True,
|
||||||
|
ticker_interval=default_conf['ticker_interval']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_tickerlist() -> None:
|
||||||
|
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_len = len(ticker_list)
|
||||||
|
|
||||||
|
# Test the pattern ^(-\d+)$
|
||||||
|
# This pattern uses the latest N elements
|
||||||
|
timerange = TimeRange(None, 'line', 0, -5)
|
||||||
|
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[-1] is ticker[-1] # The last element must be the same
|
||||||
|
|
||||||
|
# Test the pattern ^(\d+)-$
|
||||||
|
# This pattern keep X element from the end
|
||||||
|
timerange = TimeRange('line', None, 5, 0)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == 5
|
||||||
|
assert ticker_list[0] is ticker[0] # The first element must be the same
|
||||||
|
assert ticker_list[-1] is not ticker[-1] # The last element should be different
|
||||||
|
|
||||||
|
# Test the pattern ^(\d+)-(\d+)$
|
||||||
|
# This pattern extract a window
|
||||||
|
timerange = TimeRange('index', 'index', 5, 10)
|
||||||
|
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})-(\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
|
||||||
|
# This pattern must return the list unchanged
|
||||||
|
timerange = TimeRange(None, None, None, 5)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_list_len == ticker_len
|
||||||
|
|
||||||
|
# Test invalid timerange (start after stop)
|
||||||
|
timerange = TimeRange('index', 'index', 10, 5)
|
||||||
|
with pytest.raises(ValueError, match=r'The timerange .* is incorrect'):
|
||||||
|
trim_tickerlist(ticker_list, timerange)
|
||||||
|
|
||||||
|
assert ticker_list_len == ticker_len
|
||||||
|
|
||||||
|
# passing empty list
|
||||||
|
timerange = TimeRange(None, None, None, 5)
|
||||||
|
ticker = trim_tickerlist([], timerange)
|
||||||
|
assert 0 == len(ticker)
|
||||||
|
assert not ticker
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_dump_json() -> None:
|
||||||
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||||
|
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||||
|
data = {'bar': 'foo'}
|
||||||
|
|
||||||
|
# check the file we will create does not exist
|
||||||
|
assert os.path.isfile(file) is False
|
||||||
|
|
||||||
|
# Create the Json file
|
||||||
|
file_dump_json(file, data)
|
||||||
|
|
||||||
|
# Check the file was create
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
|
||||||
|
# Open the Json file created and test the data is in it
|
||||||
|
with open(file) as data_file:
|
||||||
|
json_from_file = json.load(data_file)
|
||||||
|
|
||||||
|
assert 'bar' in json_from_file
|
||||||
|
assert json_from_file['bar'] == 'foo'
|
||||||
|
|
||||||
|
# Remove the file
|
||||||
|
_clean_test_file(file)
|
0
freqtrade/tests/edge/__init__.py
Normal file
0
freqtrade/tests/edge/__init__.py
Normal file
362
freqtrade/tests/edge/test_edge.py
Normal file
362
freqtrade/tests/edge/test_edge.py
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||||
|
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.edge import Edge, PairInfo
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||||
|
from freqtrade.tests.optimize import (BTContainer, BTrade,
|
||||||
|
_build_backtest_dataframe,
|
||||||
|
_get_frame_time_from_offset)
|
||||||
|
|
||||||
|
# Cases to be tested:
|
||||||
|
# 1) Open trade should be removed from the end
|
||||||
|
# 2) Two complete trades within dataframe (with sell hit for all)
|
||||||
|
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||||
|
# 4) Entered, sl 3%, candle drops 4%, recovers to 1% => Trade closed, 3% loss
|
||||||
|
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||||
|
####################################################################
|
||||||
|
|
||||||
|
ticker_start_time = arrow.get(2018, 10, 3)
|
||||||
|
ticker_interval_in_minute = 60
|
||||||
|
_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7}
|
||||||
|
|
||||||
|
|
||||||
|
# Open trade should be removed from the end
|
||||||
|
tc0 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 1]], # enter trade (signal on last candle)
|
||||||
|
stop_loss=-0.99, roi=float('inf'), profit_perc=0.00,
|
||||||
|
trades=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two complete trades within dataframe(with sell hit for all)
|
||||||
|
tc1 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 1], # enter trade (signal on last candle)
|
||||||
|
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], # exit at open
|
||||||
|
[3, 5000, 5025, 4975, 4987, 6172, 1, 0], # no action
|
||||||
|
[4, 5000, 5025, 4975, 4987, 6172, 0, 0], # should enter the trade
|
||||||
|
[5, 5000, 5025, 4975, 4987, 6172, 0, 1], # no action
|
||||||
|
[6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell
|
||||||
|
],
|
||||||
|
stop_loss=-0.99, roi=float('inf'), profit_perc=0.00,
|
||||||
|
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=2),
|
||||||
|
BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=4, close_tick=6)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
|
||||||
|
tc2 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4600, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||||
|
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||||
|
],
|
||||||
|
stop_loss=-0.01, roi=float('inf'), profit_perc=-0.01,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss
|
||||||
|
tc3 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4800, 4987, 6172, 0, 0], # enter trade, stoploss hit
|
||||||
|
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||||
|
],
|
||||||
|
stop_loss=-0.03, roi=float('inf'), profit_perc=-0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5) Stoploss and sell are hit. should sell on stoploss
|
||||||
|
tc4 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4800, 4987, 6172, 0, 1], # enter trade, stoploss hit, sell signal
|
||||||
|
[2, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||||
|
],
|
||||||
|
stop_loss=-0.03, roi=float('inf'), profit_perc=-0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
TESTS = [
|
||||||
|
tc0,
|
||||||
|
tc1,
|
||||||
|
tc2,
|
||||||
|
tc3,
|
||||||
|
tc4
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", TESTS)
|
||||||
|
def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
||||||
|
"""
|
||||||
|
run functional tests
|
||||||
|
"""
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
frame = _build_backtest_dataframe(data.data)
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
edge.fee = 0
|
||||||
|
|
||||||
|
trades = edge._find_trades_for_stoploss_range(frame, 'TEST/BTC', [data.stop_loss])
|
||||||
|
results = edge._fill_calculable_fields(DataFrame(trades)) if trades else DataFrame()
|
||||||
|
|
||||||
|
print(results)
|
||||||
|
|
||||||
|
assert len(trades) == len(data.trades)
|
||||||
|
|
||||||
|
if not results.empty:
|
||||||
|
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||||
|
|
||||||
|
for c, trade in enumerate(data.trades):
|
||||||
|
res = results.iloc[c]
|
||||||
|
assert res.exit_type == trade.sell_reason
|
||||||
|
assert res.open_time == _get_frame_time_from_offset(trade.open_tick)
|
||||||
|
assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
|
||||||
|
|
||||||
|
|
||||||
|
def test_adjust(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
'C/D': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
'N/O': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
pairs = ['A/B', 'C/D', 'E/F', 'G/H']
|
||||||
|
assert(edge.adjust(pairs) == ['E/F', 'C/D'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
'C/D': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
'N/O': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert edge.stoploss('E/F') == -0.01
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexisting_stoploss(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
assert edge.stoploss('N/O') == -0.1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stake_amount(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
free = 100
|
||||||
|
total = 100
|
||||||
|
in_trade = 25
|
||||||
|
assert edge.stake_amount('E/F', free, total, in_trade) == 31.25
|
||||||
|
|
||||||
|
free = 20
|
||||||
|
total = 100
|
||||||
|
in_trade = 25
|
||||||
|
assert edge.stake_amount('E/F', free, total, in_trade) == 20
|
||||||
|
|
||||||
|
free = 0
|
||||||
|
total = 100
|
||||||
|
in_trade = 25
|
||||||
|
assert edge.stake_amount('E/F', free, total, in_trade) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonexisting_stake_amount(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
|
||||||
|
return_value={
|
||||||
|
'E/F': PairInfo(-0.11, 0.66, 3.71, 0.50, 1.71, 10, 60),
|
||||||
|
}
|
||||||
|
))
|
||||||
|
# should use strategy stoploss
|
||||||
|
assert edge.stake_amount('N/O', 1, 2, 1) == 0.15
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_ohlc(buy_ohlc_sell_matrice):
|
||||||
|
for index, ohlc in enumerate(buy_ohlc_sell_matrice):
|
||||||
|
# if not high < open < low or not high < close < low
|
||||||
|
if not ohlc[3] >= ohlc[2] >= ohlc[4] or not ohlc[3] >= ohlc[5] >= ohlc[4]:
|
||||||
|
raise Exception('Line ' + str(index + 1) + ' of ohlc has invalid values!')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _build_dataframe(buy_ohlc_sell_matrice):
|
||||||
|
_validate_ohlc(buy_ohlc_sell_matrice)
|
||||||
|
tickers = []
|
||||||
|
for ohlc in buy_ohlc_sell_matrice:
|
||||||
|
ticker = {
|
||||||
|
'date': ticker_start_time.shift(
|
||||||
|
minutes=(
|
||||||
|
ohlc[0] *
|
||||||
|
ticker_interval_in_minute)).timestamp *
|
||||||
|
1000,
|
||||||
|
'buy': ohlc[1],
|
||||||
|
'open': ohlc[2],
|
||||||
|
'high': ohlc[3],
|
||||||
|
'low': ohlc[4],
|
||||||
|
'close': ohlc[5],
|
||||||
|
'sell': ohlc[6]}
|
||||||
|
tickers.append(ticker)
|
||||||
|
|
||||||
|
frame = DataFrame(tickers)
|
||||||
|
frame['date'] = to_datetime(frame['date'],
|
||||||
|
unit='ms',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def _time_on_candle(number):
|
||||||
|
return np.datetime64(ticker_start_time.shift(
|
||||||
|
minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms')
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_heartbeat_calculate(mocker, edge_conf):
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
heartbeat = edge_conf['edge']['process_throttle_secs']
|
||||||
|
|
||||||
|
# should not recalculate if heartbeat not reached
|
||||||
|
edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1
|
||||||
|
|
||||||
|
assert edge.calculate() is False
|
||||||
|
|
||||||
|
|
||||||
|
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||||
|
timerange=None, exchange=None):
|
||||||
|
hz = 0.1
|
||||||
|
base = 0.001
|
||||||
|
|
||||||
|
ETHBTC = [
|
||||||
|
[
|
||||||
|
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
||||||
|
math.sin(x * hz) / 1000 + base,
|
||||||
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
|
math.sin(x * hz) / 1000 + base,
|
||||||
|
123.45
|
||||||
|
] for x in range(0, 500)]
|
||||||
|
|
||||||
|
hz = 0.2
|
||||||
|
base = 0.002
|
||||||
|
LTCBTC = [
|
||||||
|
[
|
||||||
|
ticker_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000,
|
||||||
|
math.sin(x * hz) / 1000 + base,
|
||||||
|
math.sin(x * hz) / 1000 + base + 0.0001,
|
||||||
|
math.sin(x * hz) / 1000 + base - 0.0001,
|
||||||
|
math.sin(x * hz) / 1000 + base,
|
||||||
|
123.45
|
||||||
|
] for x in range(0, 500)]
|
||||||
|
|
||||||
|
pairdata = {'NEO/BTC': parse_ticker_dataframe(ETHBTC),
|
||||||
|
'LTC/BTC': parse_ticker_dataframe(LTCBTC)}
|
||||||
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_process_downloaded_data(mocker, edge_conf):
|
||||||
|
edge_conf['datadir'] = None
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
|
||||||
|
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
|
||||||
|
assert edge.calculate()
|
||||||
|
assert len(edge._cached_pairs) == 2
|
||||||
|
assert edge._last_updated <= arrow.utcnow().timestamp + 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_expectancy(mocker, edge_conf):
|
||||||
|
edge_conf['edge']['min_trade_number'] = 2
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
|
||||||
|
def get_fee():
|
||||||
|
return 0.001
|
||||||
|
|
||||||
|
freqtrade.exchange.get_fee = get_fee
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
|
||||||
|
trades = [
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:05:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:10:00.000000000'),
|
||||||
|
'open_index': 1,
|
||||||
|
'close_index': 1,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 17,
|
||||||
|
'close_rate': 17,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||||
|
'open_index': 4,
|
||||||
|
'close_index': 4,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 20,
|
||||||
|
'close_rate': 20,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:30:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:40:00.000000000'),
|
||||||
|
'open_index': 6,
|
||||||
|
'close_index': 7,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 26,
|
||||||
|
'close_rate': 34,
|
||||||
|
'exit_type': 'sell_signal'}
|
||||||
|
]
|
||||||
|
|
||||||
|
trades_df = DataFrame(trades)
|
||||||
|
trades_df = edge._fill_calculable_fields(trades_df)
|
||||||
|
final = edge._process_expectancy(trades_df)
|
||||||
|
assert len(final) == 1
|
||||||
|
|
||||||
|
assert 'TEST/BTC' in final
|
||||||
|
assert final['TEST/BTC'].stoploss == -0.9
|
||||||
|
assert round(final['TEST/BTC'].winrate, 10) == 0.3333333333
|
||||||
|
assert round(final['TEST/BTC'].risk_reward_ratio, 10) == 306.5384615384
|
||||||
|
assert round(final['TEST/BTC'].required_risk_reward, 10) == 2.0
|
||||||
|
assert round(final['TEST/BTC'].expectancy, 10) == 101.5128205128
|
0
freqtrade/tests/exchange/__init__.py
Normal file
0
freqtrade/tests/exchange/__init__.py
Normal file
|
@ -1,5 +1,6 @@
|
||||||
# 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 copy
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
|
@ -8,6 +9,7 @@ from unittest.mock import Mock, MagicMock, PropertyMock
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import API_RETRY_COUNT, Exchange
|
from freqtrade.exchange import API_RETRY_COUNT, Exchange
|
||||||
|
@ -56,6 +58,32 @@ def test_init(default_conf, mocker, caplog):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
||||||
|
ex = Exchange(conf)
|
||||||
|
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}",
|
||||||
|
caplog.record_tuples)
|
||||||
|
assert ex._api_async.aiohttp_trust_env
|
||||||
|
assert not ex._api.aiohttp_trust_env
|
||||||
|
|
||||||
|
# Reset logging and config
|
||||||
|
caplog.clear()
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||||
|
ex = Exchange(conf)
|
||||||
|
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}",
|
||||||
|
caplog.record_tuples)
|
||||||
|
assert not ex._api_async.aiohttp_trust_env
|
||||||
|
assert hasattr(ex._api, 'TestKWARG')
|
||||||
|
assert ex._api.TestKWARG == 11
|
||||||
|
assert not hasattr(ex._api_async, 'TestKWARG')
|
||||||
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_destroy(default_conf, mocker, caplog):
|
def test_destroy(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
get_patched_exchange(mocker, default_conf)
|
get_patched_exchange(mocker, default_conf)
|
||||||
|
@ -328,6 +356,59 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_order_types(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||||
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': False
|
||||||
|
}
|
||||||
|
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
|
||||||
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'market',
|
||||||
|
'stoploss_on_exchange': 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'Exchange .* does not support market orders.'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
default_conf['order_types'] = {
|
||||||
|
'buy': 'limit',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': True
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'On exchange stoploss is not supported for .*'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_order_types_not_in_config(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
Exchange(conf)
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_has(default_conf, mocker):
|
def test_exchange_has(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
assert not exchange.exchange_has('ASDFASDF')
|
assert not exchange.exchange_has('ASDFASDF')
|
||||||
|
@ -346,7 +427,8 @@ def test_buy_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
order = exchange.buy(pair='ETH/BTC', ordertype='limit',
|
||||||
|
amount=1, rate=200, time_in_force='gtc')
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'dry_run_buy_' in order['id']
|
assert 'dry_run_buy_' in order['id']
|
||||||
|
|
||||||
|
@ -354,47 +436,106 @@ def test_buy_dry_run(default_conf, mocker):
|
||||||
def test_buy_prod(default_conf, mocker):
|
def test_buy_prod(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
api_mock.create_limit_buy_order = MagicMock(return_value={
|
order_type = 'market'
|
||||||
|
time_in_force = 'gtc'
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_type = 'limit'
|
||||||
|
order = exchange.buy(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
ordertype=order_type,
|
||||||
|
amount=1,
|
||||||
|
rate=200,
|
||||||
|
time_in_force=time_in_force)
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
|
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_considers_time_in_force(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'market'
|
||||||
|
time_in_force = 'ioc'
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
|
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||||
|
amount=1, rate=200, time_in_force=time_in_force)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'buy'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc'}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
order = exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200)
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'dry_run_sell_' in order['id']
|
assert 'dry_run_sell_' in order['id']
|
||||||
|
|
||||||
|
@ -402,7 +543,8 @@ def test_sell_dry_run(default_conf, mocker):
|
||||||
def test_sell_prod(default_conf, mocker):
|
def test_sell_prod(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||||
api_mock.create_limit_sell_order = MagicMock(return_value={
|
order_type = 'market'
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
|
@ -411,32 +553,48 @@ def test_sell_prod(default_conf, mocker):
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
|
||||||
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] is None
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_type = 'limit'
|
||||||
|
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError)
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
|
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||||
|
|
||||||
|
|
||||||
def test_get_balance_dry_run(default_conf, mocker):
|
def test_get_balance_dry_run(default_conf, mocker):
|
||||||
|
@ -545,6 +703,7 @@ def test_get_ticker(default_conf, mocker):
|
||||||
'last': 0.0001,
|
'last': 0.0001,
|
||||||
}
|
}
|
||||||
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
||||||
|
api_mock.markets = {'ETH/BTC': {}}
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
# retrieve original ticker
|
# retrieve original ticker
|
||||||
ticker = exchange.get_ticker(pair='ETH/BTC')
|
ticker = exchange.get_ticker(pair='ETH/BTC')
|
||||||
|
@ -587,6 +746,9 @@ def test_get_ticker(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'):
|
||||||
|
exchange.get_ticker(pair='XRP/ETH', refresh=True)
|
||||||
|
|
||||||
|
|
||||||
def test_get_history(default_conf, mocker, caplog):
|
def test_get_history(default_conf, mocker, caplog):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
@ -619,12 +781,20 @@ def test_get_history(default_conf, mocker, caplog):
|
||||||
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||||
tick = [
|
tick = [
|
||||||
[
|
[
|
||||||
1511686200000, # unix timestamp ms
|
(arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms
|
||||||
1, # open
|
1, # open
|
||||||
2, # high
|
2, # high
|
||||||
3, # low
|
3, # low
|
||||||
4, # close
|
4, # close
|
||||||
5, # volume (in quote currency)
|
5, # volume (in quote currency)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||||
|
3, # open
|
||||||
|
1, # high
|
||||||
|
4, # low
|
||||||
|
6, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -634,13 +804,28 @@ def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||||
|
|
||||||
pairs = ['IOTA/ETH', 'XRP/ETH']
|
pairs = ['IOTA/ETH', 'XRP/ETH']
|
||||||
# empty dicts
|
# empty dicts
|
||||||
assert not exchange.klines
|
assert not exchange._klines
|
||||||
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
||||||
|
|
||||||
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
|
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
|
||||||
assert exchange.klines
|
assert exchange._klines
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
assert exchange.klines[pair]
|
assert isinstance(exchange.klines(pair), DataFrame)
|
||||||
|
assert len(exchange.klines(pair)) > 0
|
||||||
|
|
||||||
|
# klines function should return a different object on each call
|
||||||
|
# if copy is "True"
|
||||||
|
assert exchange.klines(pair) is not exchange.klines(pair)
|
||||||
|
assert exchange.klines(pair) is not exchange.klines(pair, copy=True)
|
||||||
|
assert exchange.klines(pair, copy=True) is not exchange.klines(pair, copy=True)
|
||||||
|
assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False)
|
||||||
|
|
||||||
|
# test caching
|
||||||
|
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
||||||
|
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
|
assert log_has(f"Using cached klines data for {pairs[0]} ...", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -670,10 +855,6 @@ async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||||
assert res[1] == tick
|
assert res[1] == tick
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
||||||
# test caching
|
|
||||||
res = await exchange._async_get_candle_history(pair, "5m")
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
|
||||||
assert log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
|
||||||
|
|
||||||
# exchange = Exchange(default_conf)
|
# exchange = Exchange(default_conf)
|
||||||
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
|
@ -780,65 +961,10 @@ def make_fetch_ohlcv_mock(data):
|
||||||
return fetch_ohlcv_mock
|
return fetch_ohlcv_mock
|
||||||
|
|
||||||
|
|
||||||
def test_get_candle_history(default_conf, mocker):
|
@pytest.mark.asyncio
|
||||||
api_mock = MagicMock()
|
async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||||
tick = [
|
def sort_data(data, key):
|
||||||
[
|
return sorted(data, key=key)
|
||||||
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
|
|
||||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
|
||||||
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 ticker and ensure tick changes
|
|
||||||
new_tick = [
|
|
||||||
[
|
|
||||||
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)
|
|
||||||
|
|
||||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
|
||||||
assert ticks[0][0] == 1511686210000
|
|
||||||
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
|
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
|
||||||
"get_candle_history", "fetch_ohlcv",
|
|
||||||
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
|
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_candle_history_sort(default_conf, mocker):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
|
|
||||||
# GDAX use-case (real data from GDAX)
|
# GDAX use-case (real data from GDAX)
|
||||||
# This ticker history is ordered DESC (newest first, oldest last)
|
# This ticker history is ordered DESC (newest first, oldest last)
|
||||||
|
@ -854,13 +980,15 @@ def test_get_candle_history_sort(default_conf, mocker):
|
||||||
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
||||||
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
||||||
]
|
]
|
||||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
|
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
|
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert res[0] == 'ETH/BTC'
|
||||||
|
ticks = res[1]
|
||||||
|
|
||||||
|
assert sort_mock.call_count == 1
|
||||||
assert ticks[0][0] == 1527830400000
|
assert ticks[0][0] == 1527830400000
|
||||||
assert ticks[0][1] == 0.07649
|
assert ticks[0][1] == 0.07649
|
||||||
assert ticks[0][2] == 0.07651
|
assert ticks[0][2] == 0.07651
|
||||||
|
@ -889,11 +1017,15 @@ def test_get_candle_history_sort(default_conf, mocker):
|
||||||
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
|
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
|
||||||
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
|
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
|
||||||
]
|
]
|
||||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
# Reset sort mock
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data))
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
|
assert res[0] == 'ETH/BTC'
|
||||||
|
ticks = res[1]
|
||||||
|
# Sorted not called again - data is already in order
|
||||||
|
assert sort_mock.call_count == 0
|
||||||
assert ticks[0][0] == 1527827700000
|
assert ticks[0][0] == 1527827700000
|
||||||
assert ticks[0][1] == 0.07659999
|
assert ticks[0][1] == 0.07659999
|
||||||
assert ticks[0][2] == 0.0766
|
assert ticks[0][2] == 0.0766
|
||||||
|
@ -1076,3 +1208,85 @@ def test_get_fee(default_conf, mocker):
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
'get_fee', 'calculate_fee')
|
'get_fee', 'calculate_fee')
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_limit_order(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop_loss_limit'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args[0][1] == order_type
|
||||||
|
assert api_mock.create_order.call_args[0][2] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
|
assert api_mock.create_order.call_args[0][4] == 200
|
||||||
|
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop_loss_limit'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
46
freqtrade/tests/optimize/__init__.py
Normal file
46
freqtrade/tests/optimize/__init__.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from typing import NamedTuple, List
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.constants import TICKER_INTERVAL_MINUTES
|
||||||
|
|
||||||
|
ticker_start_time = arrow.get(2018, 10, 3)
|
||||||
|
tests_ticker_interval = "1h"
|
||||||
|
|
||||||
|
|
||||||
|
class BTrade(NamedTuple):
|
||||||
|
"""
|
||||||
|
Minimalistic Trade result used for functional backtesting
|
||||||
|
"""
|
||||||
|
sell_reason: SellType
|
||||||
|
open_tick: int
|
||||||
|
close_tick: int
|
||||||
|
|
||||||
|
|
||||||
|
class BTContainer(NamedTuple):
|
||||||
|
"""
|
||||||
|
Minimal BacktestContainer defining Backtest inputs and results.
|
||||||
|
"""
|
||||||
|
data: List[float]
|
||||||
|
stop_loss: float
|
||||||
|
roi: float
|
||||||
|
trades: List[BTrade]
|
||||||
|
profit_perc: float
|
||||||
|
|
||||||
|
|
||||||
|
def _get_frame_time_from_offset(offset):
|
||||||
|
return ticker_start_time.shift(minutes=(offset * TICKER_INTERVAL_MINUTES[tests_ticker_interval])
|
||||||
|
).datetime.replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_backtest_dataframe(ticker_with_signals):
|
||||||
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell']
|
||||||
|
|
||||||
|
frame = DataFrame.from_records(ticker_with_signals, columns=columns)
|
||||||
|
frame['date'] = frame['date'].apply(_get_frame_time_from_offset)
|
||||||
|
# Ensure floats are in place
|
||||||
|
for column in ['open', 'high', 'low', 'close', 'volume']:
|
||||||
|
frame[column] = frame[column].astype('float64')
|
||||||
|
return frame
|
182
freqtrade/tests/optimize/test_backtest_detail.py
Normal file
182
freqtrade/tests/optimize/test_backtest_detail.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataframe,
|
||||||
|
_get_frame_time_from_offset, tests_ticker_interval)
|
||||||
|
from freqtrade.tests.conftest import patch_exchange
|
||||||
|
|
||||||
|
|
||||||
|
# Test 0 Minus 8% Close
|
||||||
|
# Test with Stop-loss at 1%
|
||||||
|
# TC1: Stop-Loss Triggered 1% loss
|
||||||
|
tc0 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit
|
||||||
|
[3, 4975, 5000, 4980, 4977, 6172, 0, 0],
|
||||||
|
[4, 4977, 4987, 4977, 4995, 6172, 0, 0],
|
||||||
|
[5, 4995, 4995, 4995, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi=1, profit_perc=-0.01,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test 1 Minus 4% Low, minus 1% close
|
||||||
|
# Test with Stop-Loss at 3%
|
||||||
|
# TC2: Stop-Loss Triggered 3% Loss
|
||||||
|
tc1 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5012, 4962, 4975, 6172, 0, 0],
|
||||||
|
[3, 4975, 5000, 4800, 4962, 6172, 0, 0], # exit with stoploss hit
|
||||||
|
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||||
|
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.03, roi=1, profit_perc=-0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test 3 Candle drops 4%, Recovers 1%.
|
||||||
|
# Entry Criteria Met
|
||||||
|
# Candle drops 20%
|
||||||
|
# Candle Data for test 3
|
||||||
|
# Test with Stop-Loss at 2%
|
||||||
|
# TC3: Trade-A: Stop-Loss Triggered 2% Loss
|
||||||
|
# Trade-B: Stop-Loss Triggered 2% Loss
|
||||||
|
tc2 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5012, 4800, 4975, 6172, 0, 0], # exit with stoploss hit
|
||||||
|
[3, 4975, 5000, 4950, 4962, 6172, 1, 0],
|
||||||
|
[4, 4975, 5000, 4950, 4962, 6172, 0, 0], # enter trade 2 (signal on last candle)
|
||||||
|
[5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit
|
||||||
|
[6, 4950, 4975, 4975, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.02, roi=1, profit_perc=-0.04,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2),
|
||||||
|
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 4 Minus 3% / recovery +15%
|
||||||
|
# Candle Data for test 3 – Candle drops 3% Closed 15% up
|
||||||
|
# Test with Stop-loss at 2% ROI 6%
|
||||||
|
# TC4: Stop-Loss Triggered 2% Loss
|
||||||
|
tc3 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5750, 4850, 5750, 6172, 0, 0], # Exit with stoploss hit
|
||||||
|
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||||
|
[4, 4962, 4987, 4937, 4950, 6172, 0, 0],
|
||||||
|
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.02, roi=0.06, profit_perc=-0.02,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 4 / Drops 0.5% Closes +20%
|
||||||
|
# Set stop-loss at 1% ROI 3%
|
||||||
|
# TC5: ROI triggers 3% Gain
|
||||||
|
tc4 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4980, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5025, 4975, 4987, 6172, 0, 0],
|
||||||
|
[3, 4975, 6000, 4975, 6000, 6172, 0, 0], # ROI
|
||||||
|
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||||
|
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi=0.03, profit_perc=0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve
|
||||||
|
# Candle Data for test 6
|
||||||
|
# Set stop-loss at 2% ROI at 5%
|
||||||
|
# TC6: Stop-Loss triggers 2% Loss
|
||||||
|
tc5 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle)
|
||||||
|
[2, 4987, 5300, 4850, 5050, 6172, 0, 0], # Exit with stoploss
|
||||||
|
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||||
|
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||||
|
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.02, roi=0.05, profit_perc=-0.02,
|
||||||
|
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 7 - 6% Positive / 1% Negative / Close 1% Positve
|
||||||
|
# Candle Data for test 7
|
||||||
|
# Set stop-loss at 2% ROI at 3%
|
||||||
|
# TC7: ROI Triggers 3% Gain
|
||||||
|
tc6 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5025, 4975, 4987, 6172, 1, 0],
|
||||||
|
[1, 5000, 5025, 4975, 4987, 6172, 0, 0],
|
||||||
|
[2, 4987, 5300, 4950, 5050, 6172, 0, 0],
|
||||||
|
[3, 4975, 5000, 4950, 4962, 6172, 0, 0],
|
||||||
|
[4, 4962, 4987, 4972, 4950, 6172, 0, 0],
|
||||||
|
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.02, roi=0.03, profit_perc=0.03,
|
||||||
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)]
|
||||||
|
)
|
||||||
|
|
||||||
|
TESTS = [
|
||||||
|
tc0,
|
||||||
|
tc1,
|
||||||
|
tc2,
|
||||||
|
tc3,
|
||||||
|
tc4,
|
||||||
|
tc5,
|
||||||
|
tc6,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", TESTS)
|
||||||
|
def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
||||||
|
"""
|
||||||
|
run functional tests
|
||||||
|
"""
|
||||||
|
default_conf["stoploss"] = data.stop_loss
|
||||||
|
default_conf["minimal_roi"] = {"0": data.roi}
|
||||||
|
default_conf['ticker_interval'] = tests_ticker_interval
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0))
|
||||||
|
patch_exchange(mocker)
|
||||||
|
frame = _build_backtest_dataframe(data.data)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.advise_buy = lambda a, m: frame
|
||||||
|
backtesting.advise_sell = lambda a, m: frame
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
pair = 'UNITTEST/BTC'
|
||||||
|
# Dummy data as we mock the analyze functions
|
||||||
|
data_processed = {pair: DataFrame()}
|
||||||
|
min_date, max_date = get_timeframe({pair: frame})
|
||||||
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': data_processed,
|
||||||
|
'max_open_trades': 10,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print(results.T)
|
||||||
|
|
||||||
|
assert len(results) == len(data.trades)
|
||||||
|
assert round(results["profit_percent"].sum(), 3) == round(data.profit_perc, 3)
|
||||||
|
|
||||||
|
for c, trade in enumerate(data.trades):
|
||||||
|
res = results.iloc[c]
|
||||||
|
assert res.sell_reason == trade.sell_reason
|
||||||
|
assert res.open_time == _get_frame_time_from_offset(trade.open_tick)
|
||||||
|
assert res.close_time == _get_frame_time_from_offset(trade.close_tick)
|
|
@ -11,13 +11,16 @@ import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import DependencyException, constants, optimize
|
from freqtrade import DependencyException, constants
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.optimize import get_timeframe
|
||||||
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
||||||
start)
|
start)
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
|
||||||
from freqtrade.strategy.interface import SellType
|
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def get_args(args) -> List[str]:
|
def get_args(args) -> List[str]:
|
||||||
|
@ -33,22 +36,13 @@ def trim_dictlist(dict_list, num):
|
||||||
|
|
||||||
def load_data_test(what):
|
def load_data_test(what):
|
||||||
timerange = TimeRange(None, 'line', 0, -101)
|
timerange = TimeRange(None, 'line', 0, -101)
|
||||||
data = optimize.load_data(None, ticker_interval='1m',
|
pair = history.load_tickerdata_file(None, ticker_interval='1m',
|
||||||
pairs=['UNITTEST/BTC'], timerange=timerange)
|
pair='UNITTEST/BTC', timerange=timerange)
|
||||||
pair = data['UNITTEST/BTC']
|
|
||||||
datalen = len(pair)
|
datalen = len(pair)
|
||||||
# Depending on the what parameter we now adjust the
|
|
||||||
# loaded data looks:
|
|
||||||
# pair :: [[ 1509836520000, unix timestamp in ms
|
|
||||||
# 0.00162008, open
|
|
||||||
# 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 {'UNITTEST/BTC': [
|
data = [
|
||||||
[
|
[
|
||||||
pair[x][0], # Keep old dates
|
pair[x][0], # Keep old dates
|
||||||
x * base, # But replace O,H,L,C
|
x * base, # But replace O,H,L,C
|
||||||
|
@ -57,9 +51,9 @@ def load_data_test(what):
|
||||||
x * base,
|
x * base,
|
||||||
pair[x][5], # Keep old volume
|
pair[x][5], # Keep old volume
|
||||||
] for x in range(0, datalen)
|
] for x in range(0, datalen)
|
||||||
]}
|
]
|
||||||
if what == 'lower':
|
if what == 'lower':
|
||||||
return {'UNITTEST/BTC': [
|
data = [
|
||||||
[
|
[
|
||||||
pair[x][0], # Keep old dates
|
pair[x][0], # Keep old dates
|
||||||
1 - x * base, # But replace O,H,L,C
|
1 - x * base, # But replace O,H,L,C
|
||||||
|
@ -68,10 +62,10 @@ def load_data_test(what):
|
||||||
1 - x * base,
|
1 - x * base,
|
||||||
pair[x][5] # Keep old volume
|
pair[x][5] # Keep old volume
|
||||||
] for x in range(0, datalen)
|
] for x in range(0, datalen)
|
||||||
]}
|
]
|
||||||
if what == 'sine':
|
if what == 'sine':
|
||||||
hz = 0.1 # frequency
|
hz = 0.1 # frequency
|
||||||
return {'UNITTEST/BTC': [
|
data = [
|
||||||
[
|
[
|
||||||
pair[x][0], # Keep old dates
|
pair[x][0], # Keep old dates
|
||||||
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
|
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
|
||||||
|
@ -80,23 +74,27 @@ def load_data_test(what):
|
||||||
math.sin(x * hz) / 1000 + base,
|
math.sin(x * hz) / 1000 + base,
|
||||||
pair[x][5] # Keep old volume
|
pair[x][5] # Keep old volume
|
||||||
] for x in range(0, datalen)
|
] for x in range(0, datalen)
|
||||||
]}
|
]
|
||||||
return data
|
return {'UNITTEST/BTC': parse_ticker_dataframe(data)}
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
config['ticker_interval'] = '1m'
|
||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
processed = backtesting.tickerdata_to_dataframe(data)
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
|
@ -105,30 +103,34 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
|
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
|
||||||
timerange=None, exchange=None):
|
timerange=None, exchange=None):
|
||||||
tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
pairdata = {'UNITTEST/BTC': tickerdata}
|
pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata)}
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
# use for mock ccxt.fetch_ohlvc'
|
# use for mock ccxt.fetch_ohlvc'
|
||||||
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 = history.load_tickerdata_file(None, ticker_interval=tickfreq, pair=pair)
|
||||||
ticks = trim_dictlist(ticks, -201)
|
ticks = ticks[-201:]
|
||||||
return ticks[pair]
|
return ticks
|
||||||
|
|
||||||
|
|
||||||
# FIX: fixturize this?
|
# FIX: fixturize this?
|
||||||
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
||||||
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
|
data = history.load_data(datadir=None, ticker_interval='1m', pairs=[pair])
|
||||||
data = trim_dictlist(data, -201)
|
data = trim_dictlist(data, -201)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(conf)
|
backtesting = Backtesting(conf)
|
||||||
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
return {
|
return {
|
||||||
'stake_amount': conf['stake_amount'],
|
'stake_amount': conf['stake_amount'],
|
||||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
'processed': processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'position_stacking': False,
|
'position_stacking': False,
|
||||||
'record': record
|
'record': record,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -313,7 +315,7 @@ def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
assert backtesting.config == default_conf
|
assert backtesting.config == default_conf
|
||||||
assert backtesting.ticker_interval == '5m'
|
assert backtesting.ticker_interval == '5m'
|
||||||
assert callable(backtesting.tickerdata_to_dataframe)
|
assert callable(backtesting.strategy.tickerdata_to_dataframe)
|
||||||
assert callable(backtesting.advise_buy)
|
assert callable(backtesting.advise_buy)
|
||||||
assert callable(backtesting.advise_sell)
|
assert callable(backtesting.advise_sell)
|
||||||
get_fee.assert_called()
|
get_fee.assert_called()
|
||||||
|
@ -323,11 +325,11 @@ def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = history.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
||||||
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99
|
assert len(data['UNITTEST/BTC']) == 99
|
||||||
|
|
||||||
# Load strategy to compare the result between Backtesting function and strategy are the same
|
# Load strategy to compare the result between Backtesting function and strategy are the same
|
||||||
|
@ -336,22 +338,6 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||||
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe(default_conf, mocker) -> None:
|
|
||||||
patch_exchange(mocker)
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
|
|
||||||
data = backtesting.tickerdata_to_dataframe(
|
|
||||||
optimize.load_data(
|
|
||||||
None,
|
|
||||||
ticker_interval='1m',
|
|
||||||
pairs=['UNITTEST/BTC']
|
|
||||||
)
|
|
||||||
)
|
|
||||||
min_date, max_date = backtesting.get_timeframe(data)
|
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
|
||||||
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_generate_text_table(default_conf, mocker):
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
@ -451,21 +437,21 @@ def test_generate_text_table_strategyn(default_conf, mocker):
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
def get_timeframe(input1, input2):
|
def get_timeframe(input1):
|
||||||
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.optimize.load_data', mocked_load_data)
|
mocker.patch('freqtrade.data.history.load_data', mocked_load_data)
|
||||||
|
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
backtest=MagicMock(),
|
backtest=MagicMock(),
|
||||||
_generate_text_table=MagicMock(return_value='1'),
|
_generate_text_table=MagicMock(return_value='1'),
|
||||||
get_timeframe=get_timeframe,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
default_conf['ticker_interval'] = 1
|
default_conf['ticker_interval'] = '1m'
|
||||||
default_conf['live'] = False
|
default_conf['live'] = False
|
||||||
default_conf['datadir'] = None
|
default_conf['datadir'] = None
|
||||||
default_conf['export'] = None
|
default_conf['export'] = None
|
||||||
|
@ -486,17 +472,17 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
def get_timeframe(input1, input2):
|
def get_timeframe(input1):
|
||||||
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.optimize.load_data', MagicMock(return_value={}))
|
mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.optimize.get_timeframe', get_timeframe)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
backtest=MagicMock(),
|
backtest=MagicMock(),
|
||||||
_generate_text_table=MagicMock(return_value='1'),
|
_generate_text_table=MagicMock(return_value='1'),
|
||||||
get_timeframe=get_timeframe,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
@ -518,15 +504,19 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
pair = 'UNITTEST/BTC'
|
pair = 'UNITTEST/BTC'
|
||||||
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
timerange = TimeRange(None, 'line', 0, -201)
|
||||||
data = trim_dictlist(data, -200)
|
data = history.load_data(datadir=None, ticker_interval='5m', pairs=['UNITTEST/BTC'],
|
||||||
data_processed = backtesting.tickerdata_to_dataframe(data)
|
timerange=timerange)
|
||||||
|
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(data_processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': data_processed,
|
'processed': data_processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
@ -534,18 +524,18 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
|
|
||||||
expected = pd.DataFrame(
|
expected = pd.DataFrame(
|
||||||
{'pair': [pair, pair],
|
{'pair': [pair, pair],
|
||||||
'profit_percent': [0.00029975, 0.00056708],
|
'profit_percent': [0.0, 0.0],
|
||||||
'profit_abs': [1.49e-06, 7.6e-07],
|
'profit_abs': [0.0, 0.0],
|
||||||
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||||
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
||||||
'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime,
|
'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime,
|
||||||
Arrow(2018, 1, 30, 4, 20, 0).datetime],
|
Arrow(2018, 1, 30, 4, 15, 0).datetime],
|
||||||
'open_index': [77, 183],
|
'open_index': [78, 184],
|
||||||
'close_index': [125, 193],
|
'close_index': [125, 193],
|
||||||
'trade_duration': [240, 50],
|
'trade_duration': [235, 45],
|
||||||
'open_at_end': [False, False],
|
'open_at_end': [False, False],
|
||||||
'open_rate': [0.104445, 0.10302485],
|
'open_rate': [0.104445, 0.10302485],
|
||||||
'close_rate': [0.105, 0.10359999],
|
'close_rate': [0.104969, 0.103541],
|
||||||
'sell_reason': [SellType.ROI, SellType.ROI]
|
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||||
})
|
})
|
||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
|
@ -555,9 +545,11 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
# Check open trade rate alignes to open rate
|
# Check open trade rate alignes to open rate
|
||||||
assert ln is not None
|
assert ln is not None
|
||||||
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||||
# check close trade rate alignes to close rate
|
# check close trade rate alignes to close rate or is between high and low
|
||||||
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
||||||
assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6)
|
assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or
|
||||||
|
round(ln.iloc[0]["low"], 6) < round(
|
||||||
|
t["close_rate"], 6) < round(ln.iloc[0]["high"], 6))
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
|
@ -565,15 +557,20 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
# Run a backtesting for an exiting 5min ticker_interval
|
# Run a backtesting for an exiting 1min ticker_interval
|
||||||
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
timerange = TimeRange(None, 'line', 0, -200)
|
||||||
data = trim_dictlist(data, -200)
|
data = history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'],
|
||||||
|
timerange=timerange)
|
||||||
|
processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(processed)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
'processed': processed,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'position_stacking': False
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
@ -585,7 +582,7 @@ def test_processed(default_conf, mocker) -> None:
|
||||||
backtesting = Backtesting(default_conf)
|
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.strategy.tickerdata_to_dataframe(dict_of_tickerrows)
|
||||||
dataframe = dataframes['UNITTEST/BTC']
|
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
|
||||||
|
@ -596,26 +593,14 @@ def test_processed(default_conf, mocker) -> None:
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
|
tests = [['raise', 18], ['lower', 0], ['sine', 19]]
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
|
|
||||||
# Test backtest using offline data (testdata directory)
|
|
||||||
def test_backtest_ticks(default_conf, fee, mocker):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
ticks = [1, 5]
|
|
||||||
fun = Backtesting(default_conf).advise_buy
|
|
||||||
for _ in ticks:
|
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
|
||||||
backtesting = Backtesting(default_conf)
|
|
||||||
backtesting.advise_buy = fun # Override
|
|
||||||
backtesting.advise_sell = fun # Override
|
|
||||||
results = backtesting.backtest(backtest_conf)
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(mocker, default_conf):
|
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None, pair=None):
|
def fun(dataframe=None, pair=None):
|
||||||
|
@ -648,15 +633,94 @@ def test_backtest_only_sell(mocker, default_conf):
|
||||||
|
|
||||||
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.file_dump_json', MagicMock())
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
default_conf['ticker_interval'] = '1m'
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.advise_buy = _trend_alternate # Override
|
backtesting.advise_buy = _trend_alternate # Override
|
||||||
backtesting.advise_sell = _trend_alternate # Override
|
backtesting.advise_sell = _trend_alternate # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
backtesting._store_backtest_result("test_.json", results)
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
assert len(results) == 4
|
# 200 candles in backtest data
|
||||||
|
# won't buy on first (shifted by 1)
|
||||||
|
# 100 buys signals
|
||||||
|
assert len(results) == 100
|
||||||
# One trade was force-closed at the end
|
# One trade was force-closed at the end
|
||||||
assert len(results.loc[results.open_at_end]) == 1
|
assert len(results.loc[results.open_at_end]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_multi_pair(default_conf, fee, mocker):
|
||||||
|
|
||||||
|
def evaluate_result_multi(results, freq, max_open_trades):
|
||||||
|
# Find overlapping trades by expanding each trade once per period
|
||||||
|
# and then counting overlaps
|
||||||
|
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq))
|
||||||
|
for row in results[['open_time', 'close_time']].iterrows()]
|
||||||
|
deltas = [len(x) for x in dates]
|
||||||
|
dates = pd.Series(pd.concat(dates).values, name='date')
|
||||||
|
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
|
||||||
|
|
||||||
|
df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"})
|
||||||
|
df2 = pd.concat([dates, df2], axis=1)
|
||||||
|
df2 = df2.set_index('date')
|
||||||
|
df_final = df2.resample(freq)[['pair']].count()
|
||||||
|
return df_final[df_final['pair'] > max_open_trades]
|
||||||
|
|
||||||
|
def _trend_alternate_hold(dataframe=None, metadata=None):
|
||||||
|
"""
|
||||||
|
Buy every 8th candle - sell every other 8th -2 (hold on to pairs a bit)
|
||||||
|
"""
|
||||||
|
multi = 8
|
||||||
|
dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0)
|
||||||
|
dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0)
|
||||||
|
if metadata['pair'] in('ETH/BTC', 'LTC/BTC'):
|
||||||
|
dataframe['buy'] = dataframe['buy'].shift(-4)
|
||||||
|
dataframe['sell'] = dataframe['sell'].shift(-4)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC']
|
||||||
|
data = history.load_data(datadir=None, ticker_interval='5m', pairs=pairs)
|
||||||
|
data = trim_dictlist(data, -500)
|
||||||
|
# We need to enable sell-signal - otherwise it sells on ROI!!
|
||||||
|
default_conf['experimental'] = {"use_sell_signal": True}
|
||||||
|
default_conf['ticker_interval'] = '5m'
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
backtesting.advise_buy = _trend_alternate_hold # Override
|
||||||
|
backtesting.advise_sell = _trend_alternate_hold # Override
|
||||||
|
|
||||||
|
data_processed = backtesting.strategy.tickerdata_to_dataframe(data)
|
||||||
|
min_date, max_date = get_timeframe(data_processed)
|
||||||
|
backtest_conf = {
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': data_processed,
|
||||||
|
'max_open_trades': 3,
|
||||||
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
|
||||||
|
# Make sure we have parallel trades
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 2)) > 0
|
||||||
|
# make sure we don't have trades with more than configured max_open_trades
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 3)) == 0
|
||||||
|
|
||||||
|
backtest_conf = {
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': data_processed,
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'position_stacking': False,
|
||||||
|
'start_date': min_date,
|
||||||
|
'end_date': max_date,
|
||||||
|
}
|
||||||
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
assert len(evaluate_result_multi(results, '5min', 1)) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, fee, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
|
|
131
freqtrade/tests/optimize/test_edge_cli.py
Normal file
131
freqtrade/tests/optimize/test_edge_cli.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# pragma pylint: disable=missing-docstring, C0103, C0330
|
||||||
|
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
import json
|
||||||
|
from typing import List
|
||||||
|
from freqtrade.edge import PairInfo
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.optimize.edge_cli import (EdgeCli, setup_configuration, start)
|
||||||
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
|
def get_args(args) -> List[str]:
|
||||||
|
return Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'edge'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'stoploss_range' not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(edge_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'edge',
|
||||||
|
'--ticker-interval', '1m',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--stoplosses=-0.01,-0.10,-0.001'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1m ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(mocker, fee, edge_conf, caplog) -> None:
|
||||||
|
start_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock)
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(edge_conf)
|
||||||
|
))
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'edge'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
|
assert log_has(
|
||||||
|
'Starting freqtrade in Edge mode',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_init(mocker, edge_conf) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
edge_cli = EdgeCli(edge_conf)
|
||||||
|
assert edge_cli.config == edge_conf
|
||||||
|
assert callable(edge_cli.edge.calculate)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_edge_table(edge_conf, mocker):
|
||||||
|
patch_exchange(mocker)
|
||||||
|
edge_cli = EdgeCli(edge_conf)
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
|
||||||
|
|
||||||
|
assert edge_cli._generate_edge_table(results).count(':|') == 7
|
||||||
|
assert edge_cli._generate_edge_table(results).count('| ETH/BTC |') == 1
|
||||||
|
assert edge_cli._generate_edge_table(results).count(
|
||||||
|
'| risk reward ratio | required risk reward | expectancy |') == 1
|
|
@ -1,13 +1,15 @@
|
||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.data.history import load_tickerdata_file
|
||||||
from freqtrade.optimize.hyperopt import Hyperopt, start
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
|
@ -175,7 +177,7 @@ def test_roi_table_generation(hyperopt) -> None:
|
||||||
'roi_p3': 3,
|
'roi_p3': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||||
|
@ -194,7 +196,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||||
default_conf.update({'spaces': 'all'})
|
default_conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
hyperopt = Hyperopt(default_conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
hyperopt.strategy.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
parallel.assert_called_once()
|
parallel.assert_called_once()
|
||||||
|
@ -241,9 +243,10 @@ def test_has_space(hyperopt):
|
||||||
|
|
||||||
def test_populate_indicators(hyperopt) -> None:
|
def test_populate_indicators(hyperopt) -> None:
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
||||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
|
{'pair': '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
|
||||||
|
@ -253,11 +256,12 @@ def test_populate_indicators(hyperopt) -> None:
|
||||||
|
|
||||||
def test_buy_strategy_generator(hyperopt) -> None:
|
def test_buy_strategy_generator(hyperopt) -> None:
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
||||||
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
|
||||||
|
{'pair': 'UNITTEST/BTC'})
|
||||||
|
|
||||||
populate_buy_trend = hyperopt.buy_strategy_generator(
|
populate_buy_trend = hyperopt.custom_hyperopt.buy_strategy_generator(
|
||||||
{
|
{
|
||||||
'adx-value': 20,
|
'adx-value': 20,
|
||||||
'fastd-value': 20,
|
'fastd-value': 20,
|
||||||
|
@ -291,6 +295,10 @@ def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||||
MagicMock(return_value=backtest_result)
|
MagicMock(return_value=backtest_result)
|
||||||
)
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.get_timeframe',
|
||||||
|
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
|
||||||
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||||
|
|
||||||
|
|
|
@ -1,435 +1,64 @@
|
||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
from freqtrade import optimize, constants
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from shutil import copyfile
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
|
|
||||||
from freqtrade import optimize
|
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.data import history
|
||||||
from freqtrade.optimize.__init__ import (download_backtesting_testdata,
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
download_pairs,
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
load_cached_data_for_updating,
|
|
||||||
load_tickerdata_file,
|
|
||||||
make_testdata_path, trim_tickerlist)
|
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
|
||||||
|
|
||||||
# Change this if modifying UNITTEST/BTC testdatafile
|
|
||||||
_BTC_UNITTEST_LENGTH = 13681
|
|
||||||
|
|
||||||
|
|
||||||
def _backup_file(file: str, copy_file: bool = False) -> None:
|
def test_get_timeframe(default_conf, mocker) -> None:
|
||||||
"""
|
patch_exchange(mocker)
|
||||||
Backup existing file to avoid deleting the user file
|
strategy = DefaultStrategy(default_conf)
|
||||||
:param file: complete path to the file
|
|
||||||
:param touch_file: create an empty file in replacement
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
file_swp = file + '.swp'
|
|
||||||
if os.path.isfile(file):
|
|
||||||
os.rename(file, file_swp)
|
|
||||||
|
|
||||||
if copy_file:
|
data = strategy.tickerdata_to_dataframe(
|
||||||
copyfile(file_swp, file)
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
|
|
||||||
def _clean_test_file(file: str) -> None:
|
|
||||||
"""
|
|
||||||
Backup existing file to avoid deleting the user file
|
|
||||||
:param file: complete path to the file
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
file_swp = file + '.swp'
|
|
||||||
# 1. Delete file from the test
|
|
||||||
if os.path.isfile(file):
|
|
||||||
os.remove(file)
|
|
||||||
|
|
||||||
# 2. Rollback to the initial file
|
|
||||||
if os.path.isfile(file_swp):
|
|
||||||
os.rename(file_swp, file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
|
||||||
_backup_file(file, copy_file=True)
|
|
||||||
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
|
||||||
assert os.path.isfile(file) is True
|
|
||||||
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
|
|
||||||
_clean_test_file(file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
|
||||||
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
|
||||||
_backup_file(file, copy_file=True)
|
|
||||||
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='5m')
|
|
||||||
assert os.path.isfile(file) is True
|
|
||||||
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 5m', caplog.record_tuples)
|
|
||||||
_clean_test_file(file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
|
||||||
_backup_file(file, copy_file=True)
|
|
||||||
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
|
||||||
assert os.path.isfile(file) is True
|
|
||||||
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
|
|
||||||
_clean_test_file(file)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_conf) -> None:
|
|
||||||
"""
|
|
||||||
Test load_data() with 1 min ticker
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_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')
|
|
||||||
|
|
||||||
_backup_file(file)
|
|
||||||
# do not download a new pair if refresh_pairs isn't set
|
|
||||||
optimize.load_data(None,
|
|
||||||
ticker_interval='1m',
|
ticker_interval='1m',
|
||||||
refresh_pairs=False,
|
pairs=['UNITTEST/BTC']
|
||||||
pairs=['MEME/BTC'])
|
)
|
||||||
assert os.path.isfile(file) is False
|
)
|
||||||
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
|
min_date, max_date = optimize.get_timeframe(data)
|
||||||
'Use --refresh-pairs-cached to download the data',
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
|
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
|
data = strategy.tickerdata_to_dataframe(
|
||||||
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
|
ticker_interval='1m',
|
||||||
|
pairs=['UNITTEST/BTC']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
min_date, max_date = optimize.get_timeframe(data)
|
||||||
|
caplog.clear()
|
||||||
|
assert optimize.validate_backtest_data(data, min_date, max_date,
|
||||||
|
constants.TICKER_INTERVAL_MINUTES["1m"])
|
||||||
|
assert len(caplog.record_tuples) == 1
|
||||||
|
assert log_has(
|
||||||
|
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
|
||||||
caplog.record_tuples)
|
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 log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
|
||||||
_clean_test_file(file)
|
|
||||||
|
|
||||||
|
def test_validate_backtest_data(default_conf, mocker, caplog) -> None:
|
||||||
|
patch_exchange(mocker)
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
def test_testdata_path() -> None:
|
timerange = TimeRange('index', 'index', 200, 250)
|
||||||
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
data = strategy.tickerdata_to_dataframe(
|
||||||
|
history.load_data(
|
||||||
|
datadir=None,
|
||||||
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
ticker_interval='5m',
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
pairs=['UNITTEST/BTC'],
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
timerange=timerange
|
||||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
)
|
||||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
|
||||||
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
|
|
||||||
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
|
|
||||||
|
|
||||||
_backup_file(file1_1)
|
|
||||||
_backup_file(file1_5)
|
|
||||||
_backup_file(file2_1)
|
|
||||||
_backup_file(file2_5)
|
|
||||||
|
|
||||||
assert os.path.isfile(file1_1) is False
|
|
||||||
assert os.path.isfile(file2_1) is False
|
|
||||||
|
|
||||||
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(file2_1) is True
|
|
||||||
|
|
||||||
# clean files freshly downloaded
|
|
||||||
_clean_test_file(file1_1)
|
|
||||||
_clean_test_file(file2_1)
|
|
||||||
|
|
||||||
assert os.path.isfile(file1_5) is False
|
|
||||||
assert os.path.isfile(file2_5) is False
|
|
||||||
|
|
||||||
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(file2_5) is True
|
|
||||||
|
|
||||||
# clean files freshly downloaded
|
|
||||||
_clean_test_file(file1_5)
|
|
||||||
_clean_test_file(file2_5)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_cached_data_for_updating(mocker) -> None:
|
|
||||||
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_history', return_value=ticker_history)
|
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
|
||||||
side_effect=BaseException('File Error'))
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
|
|
||||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
|
||||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
|
||||||
_backup_file(file1_1)
|
|
||||||
_backup_file(file1_5)
|
|
||||||
|
|
||||||
download_pairs(None, exchange, pairs=['MEME/BTC'], ticker_interval='1m')
|
|
||||||
# clean files freshly downloaded
|
|
||||||
_clean_test_file(file1_1)
|
|
||||||
_clean_test_file(file1_5)
|
|
||||||
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
|
||||||
file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json')
|
|
||||||
_backup_file(file1)
|
|
||||||
download_backtesting_testdata(None, exchange, pair="XEL/BTC", tick_interval='1m')
|
|
||||||
assert os.path.isfile(file1) is True
|
|
||||||
_clean_test_file(file1)
|
|
||||||
|
|
||||||
# Download a 5 min ticker file
|
|
||||||
file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json')
|
|
||||||
_backup_file(file2)
|
|
||||||
|
|
||||||
download_backtesting_testdata(None, exchange, pair="STORJ/BTC", tick_interval='5m')
|
|
||||||
assert os.path.isfile(file2) is True
|
|
||||||
_clean_test_file(file2)
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata2(mocker, default_conf) -> None:
|
|
||||||
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)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_tickerdata_file() -> None:
|
|
||||||
# 7 does not exist in either format.
|
|
||||||
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
|
|
||||||
# 1 exists only as a .json
|
|
||||||
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
|
||||||
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
|
|
||||||
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
|
|
||||||
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker) -> None:
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
assert {} == optimize.load_data(
|
|
||||||
'',
|
|
||||||
exchange=exchange,
|
|
||||||
pairs=[],
|
|
||||||
refresh_pairs=True,
|
|
||||||
ticker_interval=default_conf['ticker_interval']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
min_date, max_date = optimize.get_timeframe(data)
|
||||||
def test_trim_tickerlist() -> None:
|
caplog.clear()
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
assert not optimize.validate_backtest_data(data, min_date, max_date,
|
||||||
with open(file) as data_file:
|
constants.TICKER_INTERVAL_MINUTES["5m"])
|
||||||
ticker_list = json.load(data_file)
|
assert len(caplog.record_tuples) == 0
|
||||||
ticker_list_len = len(ticker_list)
|
|
||||||
|
|
||||||
# Test the pattern ^(-\d+)$
|
|
||||||
# This pattern uses the latest N elements
|
|
||||||
timerange = TimeRange(None, 'line', 0, -5)
|
|
||||||
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[-1] is ticker[-1] # The last element must be the same
|
|
||||||
|
|
||||||
# Test the pattern ^(\d+)-$
|
|
||||||
# This pattern keep X element from the end
|
|
||||||
timerange = TimeRange('line', None, 5, 0)
|
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
|
||||||
ticker_len = len(ticker)
|
|
||||||
|
|
||||||
assert ticker_len == 5
|
|
||||||
assert ticker_list[0] is ticker[0] # The first element must be the same
|
|
||||||
assert ticker_list[-1] is not ticker[-1] # The last element should be different
|
|
||||||
|
|
||||||
# Test the pattern ^(\d+)-(\d+)$
|
|
||||||
# This pattern extract a window
|
|
||||||
timerange = TimeRange('index', 'index', 5, 10)
|
|
||||||
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})-(\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
|
|
||||||
# This pattern must return the list unchanged
|
|
||||||
timerange = TimeRange(None, None, None, 5)
|
|
||||||
ticker = trim_tickerlist(ticker_list, timerange)
|
|
||||||
ticker_len = len(ticker)
|
|
||||||
|
|
||||||
assert ticker_list_len == ticker_len
|
|
||||||
|
|
||||||
|
|
||||||
def test_file_dump_json() -> None:
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
|
||||||
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
|
||||||
data = {'bar': 'foo'}
|
|
||||||
|
|
||||||
# check the file we will create does not exist
|
|
||||||
assert os.path.isfile(file) is False
|
|
||||||
|
|
||||||
# Create the Json file
|
|
||||||
file_dump_json(file, data)
|
|
||||||
|
|
||||||
# Check the file was create
|
|
||||||
assert os.path.isfile(file) is True
|
|
||||||
|
|
||||||
# Open the Json file created and test the data is in it
|
|
||||||
with open(file) as data_file:
|
|
||||||
json_from_file = json.load(data_file)
|
|
||||||
|
|
||||||
assert 'bar' in json_from_file
|
|
||||||
assert json_from_file['bar'] == 'foo'
|
|
||||||
|
|
||||||
# Remove the file
|
|
||||||
_clean_test_file(file)
|
|
||||||
|
|
0
freqtrade/tests/pairlist/__init__.py
Normal file
0
freqtrade/tests/pairlist/__init__.py
Normal file
170
freqtrade/tests/pairlist/test_pairlist.py
Normal file
170
freqtrade/tests/pairlist/test_pairlist.py
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.constants import AVAILABLE_PAIRLISTS
|
||||||
|
from freqtrade.resolvers import PairListResolver
|
||||||
|
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# whitelist, blacklist
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def whitelist_conf(default_conf):
|
||||||
|
default_conf['stake_currency'] = 'BTC'
|
||||||
|
default_conf['exchange']['pair_whitelist'] = [
|
||||||
|
'ETH/BTC',
|
||||||
|
'TKN/BTC',
|
||||||
|
'TRST/BTC',
|
||||||
|
'SWT/BTC',
|
||||||
|
'BCC/BTC'
|
||||||
|
]
|
||||||
|
default_conf['exchange']['pair_blacklist'] = [
|
||||||
|
'BLK/BTC'
|
||||||
|
]
|
||||||
|
default_conf['pairlist'] = {'method': 'StaticPairList',
|
||||||
|
'config': {'number_assets': 3}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_pairlist_noexist(mocker, markets, default_conf):
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
|
with pytest.raises(ImportError,
|
||||||
|
match=r"Impossible to load Pairlist 'NonexistingPairList'."
|
||||||
|
r" This class does not exist or contains Python code errors"):
|
||||||
|
PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
|
freqtradebot.pairlists.refresh_pairlist()
|
||||||
|
# List ordered by BaseVolume
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
# Ensure all except those in whitelist are removed
|
||||||
|
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||||
|
# Ensure config dict hasn't been changed
|
||||||
|
assert (whitelist_conf['exchange']['pair_whitelist'] ==
|
||||||
|
freqtradebot.config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_pairlists(mocker, markets, whitelist_conf):
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
|
freqtradebot.pairlists.refresh_pairlist()
|
||||||
|
# List ordered by BaseVolume
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
# Ensure all except those in whitelist are removed
|
||||||
|
assert set(whitelist) == set(freqtradebot.pairlists.whitelist)
|
||||||
|
assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf):
|
||||||
|
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 5}
|
||||||
|
}
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_markets=markets,
|
||||||
|
get_tickers=tickers,
|
||||||
|
exchange_has=MagicMock(return_value=True)
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
|
||||||
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
freqtradebot.pairlists.refresh_pairlist()
|
||||||
|
|
||||||
|
assert whitelist == freqtradebot.pairlists.whitelist
|
||||||
|
|
||||||
|
whitelist_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {}
|
||||||
|
}
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'`number_assets` not specified. Please check your configuration '
|
||||||
|
r'for "pairlist.config.number_assets"'):
|
||||||
|
PairListResolver('VolumePairList', freqtradebot, whitelist_conf).pairlist
|
||||||
|
|
||||||
|
|
||||||
|
def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
||||||
|
|
||||||
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
|
whitelist = []
|
||||||
|
whitelist_conf['exchange']['pair_whitelist'] = []
|
||||||
|
freqtradebot.pairlists.refresh_pairlist()
|
||||||
|
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
assert set(whitelist) == set(pairslist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) -> None:
|
||||||
|
whitelist_conf['pairlist']['method'] = 'VolumePairList'
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
|
|
||||||
|
# Test to retrieved BTC sorted on quoteVolume (default)
|
||||||
|
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume')
|
||||||
|
assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC']
|
||||||
|
|
||||||
|
# Test to retrieve BTC sorted on bidVolume
|
||||||
|
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume')
|
||||||
|
assert whitelist == ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'BLK/BTC']
|
||||||
|
|
||||||
|
# Test with USDT sorted on quoteVolume (default)
|
||||||
|
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='USDT', key='quoteVolume')
|
||||||
|
assert whitelist == ['TKN/USDT', 'ETH/USDT', 'LTC/USDT', 'BLK/USDT']
|
||||||
|
|
||||||
|
# Test with ETH (our fixture does not have ETH, so result should be empty)
|
||||||
|
whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='ETH', key='quoteVolume')
|
||||||
|
assert whitelist == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||||
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 10}
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS)
|
||||||
|
def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
|
||||||
|
whitelist_conf['pairlist']['method'] = pairlist
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
|
||||||
|
assert freqtrade.pairlists.name == pairlist
|
||||||
|
assert pairlist in freqtrade.pairlists.short_desc()
|
||||||
|
assert isinstance(freqtrade.pairlists.whitelist, list)
|
||||||
|
assert isinstance(freqtrade.pairlists.blacklist, list)
|
||||||
|
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist)
|
||||||
|
|
||||||
|
assert set(whitelist) == set(new_whitelist)
|
||||||
|
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC', 'TRX/ETH']
|
||||||
|
new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist)
|
||||||
|
# TRX/ETH was removed
|
||||||
|
assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist)
|
||||||
|
|
||||||
|
whitelist = ['ETH/BTC', 'TKN/BTC', 'BLK/BTC']
|
||||||
|
new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist)
|
||||||
|
# BLK/BTC is in blacklist ...
|
||||||
|
assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist)
|
0
freqtrade/tests/rpc/__init__.py
Normal file
0
freqtrade/tests/rpc/__init__.py
Normal file
|
@ -7,7 +7,7 @@ from unittest.mock import MagicMock
|
||||||
import pytest
|
import pytest
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||||
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,16 +81,18 @@ def test_fiat_convert_find_price(mocker):
|
||||||
|
|
||||||
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
|
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
|
||||||
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
|
mocker.patch('freqtrade.rpc.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
|
||||||
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
|
||||||
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=13000.2)
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
|
return_value=13000.2)
|
||||||
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):
|
def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
|
||||||
|
@ -100,7 +102,8 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog):
|
||||||
def test_fiat_convert_get_price(mocker):
|
def test_fiat_convert_get_price(mocker):
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
|
return_value=28000.0)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
@ -157,7 +160,7 @@ def test_fiat_init_network_exception(mocker):
|
||||||
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||||
listmock = MagicMock(side_effect=RequestException)
|
listmock = MagicMock(side_effect=RequestException)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
listings=listmock,
|
listings=listmock,
|
||||||
)
|
)
|
||||||
# with pytest.raises(RequestEsxception):
|
# with pytest.raises(RequestEsxception):
|
||||||
|
@ -187,7 +190,7 @@ def test_fiat_invalid_response(mocker, caplog):
|
||||||
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||||
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
|
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
listings=listmock,
|
listings=listmock,
|
||||||
)
|
)
|
||||||
# with pytest.raises(RequestEsxception):
|
# with pytest.raises(RequestEsxception):
|
||||||
|
@ -203,7 +206,7 @@ def test_fiat_invalid_response(mocker, caplog):
|
||||||
|
|
||||||
def test_convert_amount(mocker):
|
def test_convert_amount(mocker):
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
result = fiat_convert.convert_amount(
|
result = fiat_convert.convert_amount(
|
|
@ -5,12 +5,13 @@ from datetime import datetime
|
||||||
from unittest.mock import MagicMock, ANY
|
from unittest.mock import MagicMock, ANY
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from numpy import isnan
|
||||||
|
|
||||||
from freqtrade import TemporaryError
|
from freqtrade import TemporaryError, DependencyException
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||||
from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
|
from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
|
||||||
|
@ -40,10 +41,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
|
||||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
|
||||||
rpc._rpc_trade_status()
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||||
rpc._rpc_trade_status()
|
rpc._rpc_trade_status()
|
||||||
|
@ -65,6 +62,27 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
'open_order': '(limit buy rem=0.00000000)'
|
'open_order': '(limit buy rem=0.00000000)'
|
||||||
} == results[0]
|
} == results[0]
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||||
|
# invalidate ticker cache
|
||||||
|
rpc._freqtrade.exchange._cached_ticker = {}
|
||||||
|
results = rpc._rpc_trade_status()
|
||||||
|
assert isnan(results[0]['current_profit'])
|
||||||
|
assert isnan(results[0]['current_rate'])
|
||||||
|
assert {
|
||||||
|
'trade_id': 1,
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'date': ANY,
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'close_rate': None,
|
||||||
|
'current_rate': ANY,
|
||||||
|
'amount': 90.99181074,
|
||||||
|
'close_profit': None,
|
||||||
|
'current_profit': ANY,
|
||||||
|
'open_order': '(limit buy rem=0.00000000)'
|
||||||
|
} == results[0]
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
@ -81,10 +99,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
|
||||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
|
||||||
rpc._rpc_status_table()
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
with pytest.raises(RPCException, match=r'.*no active order*'):
|
with pytest.raises(RPCException, match=r'.*no active order*'):
|
||||||
rpc._rpc_status_table()
|
rpc._rpc_status_table()
|
||||||
|
@ -95,6 +109,15 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
assert 'ETH/BTC' 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()
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||||
|
# invalidate ticker cache
|
||||||
|
rpc._freqtrade.exchange._cached_ticker = {}
|
||||||
|
result = rpc._rpc_status_table()
|
||||||
|
assert 'just now' in result['Since'].all()
|
||||||
|
assert 'ETH/BTC' in result['Pair'].all()
|
||||||
|
assert 'nan%' in result['Profit'].all()
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
|
@ -148,7 +171,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
@ -216,6 +239,20 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
assert stats['best_pair'] == 'ETH/BTC'
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
assert prec_satoshi(stats['best_rate'], 6.2)
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
# Test non-available pair
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
|
||||||
|
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
|
||||||
|
# invalidate ticker cache
|
||||||
|
rpc._freqtrade.exchange._cached_ticker = {}
|
||||||
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
|
assert stats['trade_count'] == 2
|
||||||
|
assert stats['first_trade_date'] == 'just now'
|
||||||
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
|
assert stats['best_pair'] == 'ETH/BTC'
|
||||||
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
assert isnan(stats['profit_all_coin'])
|
||||||
|
|
||||||
|
|
||||||
# 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)
|
||||||
|
@ -223,10 +260,11 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
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.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
|
return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
|
@ -291,7 +329,7 @@ def test_rpc_balance_handle(default_conf, mocker):
|
||||||
# ETH will be skipped due to mocked Error below
|
# ETH will be skipped due to mocked Error below
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.rpc.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
@ -532,3 +570,108 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||||
trades = rpc._rpc_count()
|
trades = rpc._rpc_count()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
assert nb_trades == 1
|
assert nb_trades == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None:
|
||||||
|
default_conf['forcebuy_enable'] = True
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
buy=buy_mm
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
trade = rpc._rpc_forcebuy(pair, None)
|
||||||
|
assert isinstance(trade, Trade)
|
||||||
|
assert trade.pair == pair
|
||||||
|
assert trade.open_rate == ticker()['ask']
|
||||||
|
|
||||||
|
# Test buy duplicate
|
||||||
|
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||||
|
rpc._rpc_forcebuy(pair, 0.0001)
|
||||||
|
pair = 'XRP/BTC'
|
||||||
|
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
||||||
|
assert isinstance(trade, Trade)
|
||||||
|
assert trade.pair == pair
|
||||||
|
assert trade.open_rate == 0.0001
|
||||||
|
|
||||||
|
# Test buy pair not with stakes
|
||||||
|
with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'):
|
||||||
|
rpc._rpc_forcebuy('XRP/ETH', 0.0001)
|
||||||
|
pair = 'XRP/BTC'
|
||||||
|
|
||||||
|
# Test not buying
|
||||||
|
default_conf['stake_amount'] = 0.0000001
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
pair = 'TKN/BTC'
|
||||||
|
trade = rpc._rpc_forcebuy(pair, None)
|
||||||
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
|
||||||
|
default_conf['forcebuy_enable'] = True
|
||||||
|
default_conf['initial_state'] = 'stopped'
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
with pytest.raises(RPCException, match=r'trader is not running'):
|
||||||
|
rpc._rpc_forcebuy(pair, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpcforcebuy_disabled(mocker, default_conf) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
with pytest.raises(RPCException, match=r'Forcebuy not enabled.'):
|
||||||
|
rpc._rpc_forcebuy(pair, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_whitelist(mocker, default_conf) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
ret = rpc._rpc_whitelist()
|
||||||
|
assert ret['method'] == 'StaticPairList'
|
||||||
|
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_whitelist_dynamic(mocker, default_conf) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 4}
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
ret = rpc._rpc_whitelist()
|
||||||
|
assert ret['method'] == 'VolumePairList'
|
||||||
|
assert ret['length'] == 4
|
||||||
|
assert ret['whitelist'] == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
|
@ -113,3 +113,25 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
||||||
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||||
assert len(rpc_manager.registered_modules) == 1
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
|
|
||||||
|
def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 3
|
||||||
|
assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status']
|
||||||
|
|
||||||
|
telegram_mock.reset_mock()
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
default_conf['whitelist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 20}
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists)
|
||||||
|
assert telegram_mock.call_count == 3
|
||||||
|
assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status']
|
||||||
|
|
|
@ -17,6 +17,7 @@ from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
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, log_has,
|
||||||
patch_exchange)
|
patch_exchange)
|
||||||
|
@ -71,8 +72,9 @@ def test_init(default_conf, mocker, caplog) -> None:
|
||||||
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'], ['forcebuy'], " \
|
||||||
"['count'], ['reload_conf'], ['help'], ['version']]"
|
"['performance'], ['daily'], ['count'], ['reload_conf'], " \
|
||||||
|
"['whitelist'], ['help'], ['version']]"
|
||||||
|
|
||||||
assert log_has(message_str, caplog.record_tuples)
|
assert log_has(message_str, caplog.record_tuples)
|
||||||
|
|
||||||
|
@ -250,9 +252,10 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
# Status is also enabled when stopped
|
||||||
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 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
@ -295,9 +298,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
# Status table is also enabled when stopped
|
||||||
telegram._status_table(bot=MagicMock(), update=update)
|
telegram._status_table(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
assert 'no active order' in msg_mock.call_args_list[0][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
@ -507,6 +511,11 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
'total': 10.0,
|
'total': 10.0,
|
||||||
'free': 10.0,
|
'free': 10.0,
|
||||||
'used': 0.0
|
'used': 0.0
|
||||||
|
},
|
||||||
|
'XRP': {
|
||||||
|
'total': 1.0,
|
||||||
|
'free': 1.0,
|
||||||
|
'used': 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +526,12 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
'ask': 10000.00,
|
'ask': 10000.00,
|
||||||
'last': 10000.00,
|
'last': 10000.00,
|
||||||
}
|
}
|
||||||
|
elif symbol == 'XRP/BTC':
|
||||||
|
return {
|
||||||
|
'bid': 0.00001,
|
||||||
|
'ask': 0.00001,
|
||||||
|
'last': 0.00001,
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
'bid': 0.1,
|
'bid': 0.1,
|
||||||
'ask': 0.1,
|
'ask': 0.1,
|
||||||
|
@ -548,7 +562,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
assert '*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: 14.00000000' in result
|
assert 'BTC: 12.00000000' in result
|
||||||
|
assert '*XRP:* not showing <1$ amount' in result
|
||||||
|
|
||||||
|
|
||||||
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||||
|
@ -712,16 +727,18 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
'profit_percent': 0.06110514,
|
'profit_percent': 0.0611052,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.FORCE_SELL.value
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
ticker_sell_down, markets, mocker) -> None:
|
ticker_sell_down, markets, mocker) -> None:
|
||||||
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.rpc.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.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -765,16 +782,18 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
'profit_percent': -0.05478343,
|
'profit_percent': -0.05478342,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.FORCE_SELL.value
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.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('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
||||||
|
@ -810,15 +829,17 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.098e-05,
|
'current_rate': 1.098e-05,
|
||||||
'profit_amount': -5.91e-06,
|
'profit_amount': -5.91e-06,
|
||||||
'profit_percent': -0.00589292,
|
'profit_percent': -0.00589291,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.FORCE_SELL.value
|
||||||
} == msg
|
} == msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
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.rpc.fiat_convert.CryptoToFiatConverter._find_price',
|
||||||
|
return_value=15000.0)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
@ -855,6 +876,63 @@ 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_forcebuy_handle(default_conf, update, markets, mocker) -> None:
|
||||||
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
fbuy_mock = MagicMock(return_value=None)
|
||||||
|
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
update.message.text = '/forcebuy ETH/BTC'
|
||||||
|
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
|
assert fbuy_mock.call_count == 1
|
||||||
|
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||||
|
assert fbuy_mock.call_args_list[0][0][1] is None
|
||||||
|
|
||||||
|
# Reset and retry with specified price
|
||||||
|
fbuy_mock = MagicMock(return_value=None)
|
||||||
|
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||||
|
update.message.text = '/forcebuy ETH/BTC 0.055'
|
||||||
|
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
|
assert fbuy_mock.call_count == 1
|
||||||
|
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
||||||
|
assert isinstance(fbuy_mock.call_args_list[0][0][1], float)
|
||||||
|
assert fbuy_mock.call_args_list[0][0][1] == 0.055
|
||||||
|
|
||||||
|
|
||||||
|
def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None:
|
||||||
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
update.message.text = '/forcebuy ETH/Nonepair'
|
||||||
|
telegram._forcebuy(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.'
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, update, ticker, fee,
|
def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
@ -895,26 +973,6 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
assert '<code>ETH/BTC\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:
|
|
||||||
patch_coinmarketcap(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
|
||||||
_init=MagicMock(),
|
|
||||||
_send_msg=msg_mock
|
|
||||||
)
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
|
||||||
telegram = Telegram(freqtradebot)
|
|
||||||
|
|
||||||
# Trader is not running
|
|
||||||
freqtradebot.state = State.STOPPED
|
|
||||||
telegram._performance(bot=MagicMock(), update=update)
|
|
||||||
assert msg_mock.call_count == 1
|
|
||||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
@ -956,6 +1014,46 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||||
assert msg in msg_mock.call_args_list[0][0][0]
|
assert msg in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_whitelist_static(default_conf, update, mocker) -> None:
|
||||||
|
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)
|
||||||
|
|
||||||
|
telegram._whitelist(bot=MagicMock(), update=update)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||||
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
|
||||||
|
def test_whitelist_dynamic(default_conf, update, mocker) -> None:
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 4}
|
||||||
|
}
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
telegram._whitelist(bot=MagicMock(), update=update)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`'
|
||||||
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
|
||||||
def test_help_handle(default_conf, update, mocker) -> None:
|
def test_help_handle(default_conf, update, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
@ -1039,16 +1137,18 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_percent': -0.57405275,
|
'profit_percent': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD'
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Binance:* Selling [KEY/ETH]' \
|
== ('*Binance:* Selling [KEY/ETH]'
|
||||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n'
|
||||||
'*Limit:* `0.00003201`\n' \
|
'*Limit:* `0.00003201`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Current Rate:* `0.00003201`\n'
|
||||||
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
|
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`')
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
telegram.send_msg({
|
telegram.send_msg({
|
||||||
|
@ -1064,15 +1164,17 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_percent': -0.57405275,
|
'profit_percent': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Binance:* Selling [KEY/ETH]' \
|
== ('*Binance:* Selling [KEY/ETH]'
|
||||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n'
|
||||||
'*Limit:* `0.00003201`\n' \
|
'*Limit:* `0.00003201`\n'
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n'
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Open Rate:* `0.00007500`\n'
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Current Rate:* `0.00003201`\n'
|
||||||
'*Profit:* `-57.41%`'
|
'*Sell Reason:* `stop_loss`\n'
|
||||||
|
'*Profit:* `-57.41%`')
|
||||||
# Reset singleton function to avoid random breaks
|
# Reset singleton function to avoid random breaks
|
||||||
telegram._fiat_converter.convert_amount = old_convamount
|
telegram._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
|
@ -1190,7 +1292,8 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
'profit_amount': -0.05746268,
|
'profit_amount': -0.05746268,
|
||||||
'profit_percent': -0.57405275,
|
'profit_percent': -0.57405275,
|
||||||
'stake_currency': 'ETH',
|
'stake_currency': 'ETH',
|
||||||
'fiat_currency': 'USD'
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
})
|
})
|
||||||
assert msg_mock.call_args[0][0] \
|
assert msg_mock.call_args[0][0] \
|
||||||
== '*Binance:* Selling [KEY/ETH]' \
|
== '*Binance:* Selling [KEY/ETH]' \
|
||||||
|
@ -1199,6 +1302,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
'*Amount:* `1333.33333333`\n' \
|
'*Amount:* `1333.33333333`\n' \
|
||||||
'*Open Rate:* `0.00007500`\n' \
|
'*Open Rate:* `0.00007500`\n' \
|
||||||
'*Current Rate:* `0.00003201`\n' \
|
'*Current Rate:* `0.00003201`\n' \
|
||||||
|
'*Sell Reason:* `stop_loss`\n' \
|
||||||
'*Profit:* `-57.41%`'
|
'*Profit:* `-57.41%`'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from requests import RequestException
|
||||||
|
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.rpc.webhook import Webhook
|
from freqtrade.rpc.webhook import Webhook
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,6 +81,7 @@ def test_send_msg(default_conf, mocker):
|
||||||
'profit_amount': 0.001,
|
'profit_amount': 0.001,
|
||||||
'profit_percent': 0.20,
|
'profit_percent': 0.20,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
}
|
}
|
||||||
webhook.send_msg(msg=msg)
|
webhook.send_msg(msg=msg)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
|
|
0
freqtrade/tests/strategy/__init__.py
Normal file
0
freqtrade/tests/strategy/__init__.py
Normal file
|
@ -3,7 +3,7 @@ import json
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.data.history import load_tickerdata_file
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
@ -16,62 +17,69 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
_STRATEGY = DefaultStrategy(config={})
|
_STRATEGY = DefaultStrategy(config={})
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker, default_conf):
|
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||||
|
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker, default_conf):
|
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
|
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
|
||||||
|
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
)
|
)
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||||
None)
|
DataFrame())
|
||||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
|
||||||
|
[])
|
||||||
|
assert log_has('Empty ticker history for pair bar', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
side_effect=ValueError('xyz')
|
side_effect=ValueError('xyz')
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], 1)
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||||
|
ticker_history)
|
||||||
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, ticker_history):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame([])
|
return_value=DataFrame([])
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||||
|
ticker_history)
|
||||||
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, ticker_history):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||||
# this is necessary as the last candle is removed (partial candles) by default
|
# this is necessary as the last candle is removed (partial candles) by default
|
||||||
|
@ -81,7 +89,8 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
return_value=DataFrame(ticks)
|
return_value=DataFrame(ticks)
|
||||||
)
|
)
|
||||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
|
||||||
|
ticker_history)
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Outdated history for pair xyz. Last tick is 16 minutes old',
|
'Outdated history for pair xyz. Last tick is 16 minutes old',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
|
@ -102,7 +111,7 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
||||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||||
|
|
||||||
|
@ -179,6 +188,10 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
strategy.process_only_new_candles = True
|
strategy.process_only_new_candles = True
|
||||||
|
|
||||||
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
|
assert 'high' in ret.columns
|
||||||
|
assert 'low' in ret.columns
|
||||||
|
assert 'close' in ret.columns
|
||||||
|
assert isinstance(ret, DataFrame)
|
||||||
assert ind_mock.call_count == 1
|
assert ind_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
|
@ -193,8 +206,8 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
|
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
|
||||||
assert 'buy' in ret
|
assert 'buy' in ret.columns
|
||||||
assert 'sell' in ret
|
assert 'sell' in ret.columns
|
||||||
assert ret['buy'].sum() == 0
|
assert ret['buy'].sum() == 0
|
||||||
assert ret['sell'].sum() == 0
|
assert ret['sell'].sum() == 0
|
||||||
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import logging
|
import logging
|
||||||
from base64 import urlsafe_b64encode
|
from base64 import urlsafe_b64encode
|
||||||
from os import path
|
from os import path
|
||||||
|
from pathlib import Path
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -10,7 +11,7 @@ from pandas import DataFrame
|
||||||
from freqtrade.strategy import import_strategy
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
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.resolvers import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
def test_import_strategy(caplog):
|
def test_import_strategy(caplog):
|
||||||
|
@ -40,21 +41,21 @@ def test_import_strategy(caplog):
|
||||||
|
|
||||||
def test_search_strategy():
|
def test_search_strategy():
|
||||||
default_config = {}
|
default_config = {}
|
||||||
default_location = path.join(path.dirname(
|
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||||
path.realpath(__file__)), '..', '..', 'strategy'
|
|
||||||
)
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
StrategyResolver._search_strategy(
|
StrategyResolver._search_object(
|
||||||
default_location,
|
directory=default_location,
|
||||||
config=default_config,
|
object_type=IStrategy,
|
||||||
strategy_name='DefaultStrategy'
|
kwargs={'config': default_config},
|
||||||
|
object_name='DefaultStrategy'
|
||||||
),
|
),
|
||||||
IStrategy
|
IStrategy
|
||||||
)
|
)
|
||||||
assert StrategyResolver._search_strategy(
|
assert StrategyResolver._search_object(
|
||||||
default_location,
|
directory=default_location,
|
||||||
config=default_config,
|
object_type=IStrategy,
|
||||||
strategy_name='NotFoundStrategy'
|
kwargs={'config': default_config},
|
||||||
|
object_name='NotFoundStrategy'
|
||||||
) is None
|
) is None
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +78,7 @@ def test_load_strategy_invalid_directory(result, caplog):
|
||||||
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
'freqtrade.strategy.resolver',
|
'freqtrade.resolvers.strategy_resolver',
|
||||||
logging.WARNING,
|
logging.WARNING,
|
||||||
'Path "{}" does not exist'.format(extra_dir),
|
'Path "{}" does not exist'.format(extra_dir),
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
@ -88,8 +89,8 @@ def test_load_strategy_invalid_directory(result, caplog):
|
||||||
def test_load_not_found_strategy():
|
def test_load_not_found_strategy():
|
||||||
strategy = StrategyResolver()
|
strategy = StrategyResolver()
|
||||||
with pytest.raises(ImportError,
|
with pytest.raises(ImportError,
|
||||||
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
|
match=r"Impossible to load Strategy 'NotFoundStrategy'."
|
||||||
r' This class does not exist or contains Python code errors'):
|
r" This class does not exist or contains Python code errors"):
|
||||||
strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
|
strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ def test_strategy_override_minimal_roi(caplog):
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
|
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
@ -143,7 +144,7 @@ def test_strategy_override_stoploss(caplog):
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert resolver.strategy.stoploss == -0.5
|
assert resolver.strategy.stoploss == -0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override strategy 'stoploss' with value in config file: -0.5."
|
"Override strategy 'stoploss' with value in config file: -0.5."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
@ -159,7 +160,7 @@ def test_strategy_override_ticker_interval(caplog):
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert resolver.strategy.ticker_interval == 60
|
assert resolver.strategy.ticker_interval == 60
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override strategy 'ticker_interval' with value in config file: 60."
|
"Override strategy 'ticker_interval' with value in config file: 60."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
@ -175,13 +176,86 @@ def test_strategy_override_process_only_new_candles(caplog):
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert resolver.strategy.process_only_new_candles
|
assert resolver.strategy.process_only_new_candles
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
"Override process_only_new_candles 'process_only_new_candles' "
|
"Override process_only_new_candles 'process_only_new_candles' "
|
||||||
"with value in config file: True."
|
"with value in config file: True."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_order_types(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
'buy': 'market',
|
||||||
|
'sell': 'limit',
|
||||||
|
'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_types': order_types
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.order_types
|
||||||
|
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
|
||||||
|
assert resolver.strategy.order_types[method] == order_types[method]
|
||||||
|
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'order_types' with value in config file:"
|
||||||
|
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
|
||||||
|
" 'stoploss_on_exchange': True}."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_types': {'buy': 'market'}
|
||||||
|
}
|
||||||
|
# Raise error for invalid configuration
|
||||||
|
with pytest.raises(ImportError,
|
||||||
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
|
r"Order-types mapping is incomplete."):
|
||||||
|
StrategyResolver(config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_order_tif(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
order_time_in_force = {
|
||||||
|
'buy': 'fok',
|
||||||
|
'sell': 'gtc',
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_time_in_force': order_time_in_force
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.order_time_in_force
|
||||||
|
for method in ['buy', 'sell']:
|
||||||
|
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
|
||||||
|
|
||||||
|
assert ('freqtrade.resolvers.strategy_resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override strategy 'order_time_in_force' with value in config file:"
|
||||||
|
" {'buy': 'fok', 'sell': 'gtc'}."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'order_time_in_force': {'buy': 'fok'}
|
||||||
|
}
|
||||||
|
# Raise error for invalid configuration
|
||||||
|
with pytest.raises(ImportError,
|
||||||
|
match=r"Impossible to load Strategy 'DefaultStrategy'. "
|
||||||
|
r"Order-time-in-force mapping is incomplete."):
|
||||||
|
StrategyResolver(config)
|
||||||
|
|
||||||
|
|
||||||
def test_deprecate_populate_indicators(result):
|
def test_deprecate_populate_indicators(result):
|
||||||
default_location = path.join(path.dirname(path.realpath(__file__)))
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||||
|
@ -226,13 +300,13 @@ def test_call_deprecated_function(result, monkeypatch):
|
||||||
assert resolver.strategy._sell_fun_len == 2
|
assert resolver.strategy._sell_fun_len == 2
|
||||||
|
|
||||||
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
assert type(indicator_df) is DataFrame
|
assert isinstance(indicator_df, DataFrame)
|
||||||
assert 'adx' in indicator_df.columns
|
assert 'adx' in indicator_df.columns
|
||||||
|
|
||||||
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||||
assert type(buydf) is DataFrame
|
assert isinstance(buydf, DataFrame)
|
||||||
assert 'buy' in buydf.columns
|
assert 'buy' in buydf.columns
|
||||||
|
|
||||||
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||||
assert type(selldf) is DataFrame
|
assert isinstance(selldf, DataFrame)
|
||||||
assert 'sell' in selldf
|
assert 'sell' in selldf
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
# whitelist, blacklist, filtering, all of that will
|
|
||||||
# eventually become some rules to run on a generic ACL engine
|
|
||||||
# perhaps try to anticipate that by using some python package
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def whitelist_conf(default_conf):
|
|
||||||
default_conf['stake_currency'] = 'BTC'
|
|
||||||
default_conf['exchange']['pair_whitelist'] = [
|
|
||||||
'ETH/BTC',
|
|
||||||
'TKN/BTC',
|
|
||||||
'TRST/BTC',
|
|
||||||
'SWT/BTC',
|
|
||||||
'BCC/BTC'
|
|
||||||
]
|
|
||||||
default_conf['exchange']['pair_blacklist'] = [
|
|
||||||
'BLK/BTC'
|
|
||||||
]
|
|
||||||
|
|
||||||
return default_conf
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
|
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
|
||||||
whitelist_conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
|
||||||
)
|
|
||||||
# List ordered by BaseVolume
|
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
|
||||||
# Ensure all except those in whitelist are removed
|
|
||||||
assert whitelist == refreshedwhitelist
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist(mocker, markets, whitelist_conf):
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
|
||||||
whitelist_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
# List ordered by BaseVolume
|
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
|
||||||
# Ensure all except those in whitelist are removed
|
|
||||||
assert whitelist == refreshedwhitelist
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic(mocker, markets, tickers, whitelist_conf):
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
get_markets=markets,
|
|
||||||
get_tickers=tickers,
|
|
||||||
exchange_has=MagicMock(return_value=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
|
||||||
|
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
|
||||||
freqtradebot._gen_pair_whitelist(whitelist_conf['stake_currency'])
|
|
||||||
)
|
|
||||||
|
|
||||||
assert whitelist == refreshedwhitelist
|
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty, whitelist_conf):
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
|
||||||
whitelist = []
|
|
||||||
whitelist_conf['exchange']['pair_whitelist'] = []
|
|
||||||
freqtradebot._refresh_whitelist(whitelist)
|
|
||||||
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
assert set(whitelist) == set(pairslist)
|
|
|
@ -17,7 +17,8 @@ def test_parse_args_none() -> None:
|
||||||
def test_parse_args_defaults() -> None:
|
def test_parse_args_defaults() -> None:
|
||||||
args = Arguments([], '').get_parsed_arg()
|
args = Arguments([], '').get_parsed_arg()
|
||||||
assert args.config == 'config.json'
|
assert args.config == 'config.json'
|
||||||
assert args.dynamic_whitelist is None
|
assert args.strategy_path is None
|
||||||
|
assert args.datadir is None
|
||||||
assert args.loglevel == 0
|
assert args.loglevel == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate, ValidationError
|
from jsonschema import validate, ValidationError, Draft4Validator
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
@ -64,6 +64,22 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
|
||||||
|
default_conf['max_open_trades'] = -1
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
print(validated_conf)
|
||||||
|
|
||||||
|
assert validated_conf['max_open_trades'] > 999999999
|
||||||
|
assert validated_conf['max_open_trades'] == float('inf')
|
||||||
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_file_exception(mocker) -> None:
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.open',
|
'freqtrade.configuration.open',
|
||||||
|
@ -86,7 +102,7 @@ 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 'edge' not in validated_conf
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
|
@ -103,7 +119,8 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
assert validated_conf.get('dynamic_whitelist') == 10
|
assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList'
|
||||||
|
assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 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('db_url') == 'sqlite:///someurl'
|
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||||
|
@ -116,7 +133,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--strategy-path', '/some/path'
|
'--strategy-path', '/some/path'
|
||||||
]
|
]
|
||||||
|
@ -135,7 +151,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--strategy-path', '/some/path'
|
'--strategy-path', '/some/path'
|
||||||
]
|
]
|
||||||
|
@ -178,8 +193,9 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
||||||
configuration.get_config()
|
configuration.get_config()
|
||||||
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Parameter --dynamic-whitelist detected. '
|
'Parameter --dynamic-whitelist has been deprecated, '
|
||||||
'Using dynamically generated whitelist. '
|
'and will be completely replaced by the whitelist dict in the future. '
|
||||||
|
'For now: using dynamically generated whitelist based on VolumePairList. '
|
||||||
'(not applicable with Backtesting and Hyperopt)',
|
'(not applicable with Backtesting and Hyperopt)',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
@ -371,7 +387,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
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:
|
def test_check_exchange(default_conf, caplog) -> None:
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test a valid exchange
|
||||||
|
@ -392,6 +408,15 @@ def test_check_exchange(default_conf) -> None:
|
||||||
):
|
):
|
||||||
configuration.check_exchange(default_conf)
|
configuration.check_exchange(default_conf)
|
||||||
|
|
||||||
|
# Test ccxt_rate_limit depreciation
|
||||||
|
default_conf.get('exchange').update({'name': 'binance'})
|
||||||
|
default_conf['exchange']['ccxt_rate_limit'] = True
|
||||||
|
configuration.check_exchange(default_conf)
|
||||||
|
assert log_has("`ccxt_rate_limit` has been deprecated in favor of "
|
||||||
|
"`ccxt_config` and `ccxt_async_config` and will be removed "
|
||||||
|
"in a future version.",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
@ -446,5 +471,19 @@ def test_set_loggers() -> None:
|
||||||
assert logging.getLogger('telegram').level is logging.INFO
|
assert logging.getLogger('telegram').level is logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
|
||||||
|
default_conf['forcebuy_enable'] = True
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert validated_conf.get('forcebuy_enable')
|
||||||
|
assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_default_conf(default_conf) -> None:
|
def test_validate_default_conf(default_conf) -> None:
|
||||||
validate(default_conf, constants.CONF_SCHEMA)
|
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
|
||||||
|
|
||||||
import pandas
|
|
||||||
|
|
||||||
from freqtrade.optimize import load_data
|
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
|
||||||
|
|
||||||
_pairs = ['ETH/BTC']
|
|
||||||
|
|
||||||
|
|
||||||
def load_dataframe_pair(pairs, strategy):
|
|
||||||
ld = load_data(None, ticker_interval='5m', pairs=pairs)
|
|
||||||
assert isinstance(ld, dict)
|
|
||||||
assert isinstance(pairs[0], str)
|
|
||||||
dataframe = ld[pairs[0]]
|
|
||||||
|
|
||||||
dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]})
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_load():
|
|
||||||
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
|
||||||
dataframe = load_dataframe_pair(_pairs, strategy)
|
|
||||||
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_columns_exists():
|
|
||||||
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
|
||||||
dataframe = load_dataframe_pair(_pairs, strategy)
|
|
||||||
assert 'high' in dataframe.columns
|
|
||||||
assert 'low' in dataframe.columns
|
|
||||||
assert 'close' in dataframe.columns
|
|
|
@ -18,7 +18,7 @@ from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType, SellCheckTuple
|
from freqtrade.strategy.interface import SellType, SellCheckTuple
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange, patch_edge, patch_wallet
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -136,42 +136,6 @@ def test_throttle_with_assets(mocker, default_conf) -> None:
|
||||||
assert result == -1
|
assert result == -1
|
||||||
|
|
||||||
|
|
||||||
def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None:
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
|
||||||
|
|
||||||
# Test to retrieved BTC sorted on quoteVolume (default)
|
|
||||||
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC')
|
|
||||||
assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC']
|
|
||||||
|
|
||||||
# Test to retrieve BTC sorted on bidVolume
|
|
||||||
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC', key='bidVolume')
|
|
||||||
assert whitelist == ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'BLK/BTC']
|
|
||||||
|
|
||||||
# Test with USDT sorted on quoteVolume (default)
|
|
||||||
whitelist = freqtrade._gen_pair_whitelist(base_currency='USDT')
|
|
||||||
assert whitelist == ['TKN/USDT', 'ETH/USDT', 'LTC/USDT', 'BLK/USDT']
|
|
||||||
|
|
||||||
# Test with ETH (our fixture does not have ETH, so result should be empty)
|
|
||||||
whitelist = freqtrade._gen_pair_whitelist(base_currency='ETH')
|
|
||||||
assert whitelist == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
|
||||||
freqtrade._gen_pair_whitelist(base_currency='BTC')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test not implemented")
|
|
||||||
def test_refresh_whitelist() -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
@ -182,25 +146,18 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
result = freqtrade._get_trade_stake_amount()
|
result = freqtrade._get_trade_stake_amount('ETH/BTC')
|
||||||
assert result == default_conf['stake_amount']
|
assert result == default_conf['stake_amount']
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount_no_stake_amount(default_conf,
|
def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
|
||||||
ticker,
|
|
||||||
limit_buy_order,
|
|
||||||
fee,
|
|
||||||
mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)
|
|
||||||
)
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
||||||
freqtrade._get_trade_stake_amount()
|
freqtrade._get_trade_stake_amount('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
||||||
|
@ -211,12 +168,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
||||||
mocker) -> None:
|
mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
patch_wallet(mocker, free=default_conf['stake_amount'])
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount']),
|
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
@ -229,28 +186,164 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
# no open trades, order amount should be 'balance / max_open_trades'
|
# no open trades, order amount should be 'balance / max_open_trades'
|
||||||
result = freqtrade._get_trade_stake_amount()
|
result = freqtrade._get_trade_stake_amount('ETH/BTC')
|
||||||
assert result == default_conf['stake_amount'] / conf['max_open_trades']
|
assert result == default_conf['stake_amount'] / conf['max_open_trades']
|
||||||
|
|
||||||
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
|
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
result = freqtrade._get_trade_stake_amount()
|
result = freqtrade._get_trade_stake_amount('LTC/BTC')
|
||||||
assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1)
|
assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1)
|
||||||
|
|
||||||
# create 2 trades, order amount should be None
|
# create 2 trades, order amount should be None
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
result = freqtrade._get_trade_stake_amount()
|
result = freqtrade._get_trade_stake_amount('XRP/BTC')
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
# set max_open_trades = None, so do not trade
|
# set max_open_trades = None, so do not trade
|
||||||
conf['max_open_trades'] = 0
|
conf['max_open_trades'] = 0
|
||||||
freqtrade = FreqtradeBot(conf)
|
freqtrade = FreqtradeBot(conf)
|
||||||
result = freqtrade._get_trade_stake_amount()
|
result = freqtrade._get_trade_stake_amount('NEO/BTC')
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_called_in_process(mocker, edge_conf) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_edge(mocker)
|
||||||
|
|
||||||
|
def _refresh_whitelist(list):
|
||||||
|
return ['ETH/BTC', 'LTC/BTC', 'XRP/BTC', 'NEO/BTC']
|
||||||
|
|
||||||
|
patch_exchange(mocker)
|
||||||
|
freqtrade = FreqtradeBot(edge_conf)
|
||||||
|
freqtrade.pairlists._validate_whitelist = _refresh_whitelist
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade._process()
|
||||||
|
assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC']
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
patch_edge(mocker)
|
||||||
|
freqtrade = FreqtradeBot(edge_conf)
|
||||||
|
|
||||||
|
assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20
|
||||||
|
assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None:
|
||||||
|
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
patch_edge(mocker)
|
||||||
|
|
||||||
|
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
|
||||||
|
# Thus, if price falls 21%, stoploss should be triggered
|
||||||
|
#
|
||||||
|
# mocking the ticker: price is falling ...
|
||||||
|
buy_price = limit_buy_order['price']
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': buy_price * 0.79,
|
||||||
|
'ask': buy_price * 0.79,
|
||||||
|
'last': buy_price * 0.79
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
)
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Create a trade with "limit_buy_order" price
|
||||||
|
freqtrade = FreqtradeBot(edge_conf)
|
||||||
|
freqtrade.active_pair_whitelist = ['NEO/BTC']
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# stoploss shoud be hit
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog.record_tuples)
|
||||||
|
assert trade.sell_reason == SellType.STOP_LOSS.value
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets,
|
||||||
|
mocker, edge_conf) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
patch_edge(mocker)
|
||||||
|
|
||||||
|
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
|
||||||
|
# Thus, if price falls 15%, stoploss should not be triggered
|
||||||
|
#
|
||||||
|
# mocking the ticker: price is falling ...
|
||||||
|
buy_price = limit_buy_order['price']
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': buy_price * 0.85,
|
||||||
|
'ask': buy_price * 0.85,
|
||||||
|
'last': buy_price * 0.85
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
)
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# Create a trade with "limit_buy_order" price
|
||||||
|
freqtrade = FreqtradeBot(edge_conf)
|
||||||
|
freqtrade.active_pair_whitelist = ['NEO/BTC']
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
#############################################
|
||||||
|
|
||||||
|
# stoploss shoud not be hit
|
||||||
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_open_trades_stakes(mocker, default_conf, ticker,
|
||||||
|
limit_buy_order, fee, markets) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
default_conf['stake_amount'] = 0.0000098751
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
|
||||||
|
assert trade is not None
|
||||||
|
assert trade.stake_amount == 0.0000098751
|
||||||
|
assert trade.is_open
|
||||||
|
assert trade.open_date is not None
|
||||||
|
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.order_by(Trade.id.desc()).first()
|
||||||
|
|
||||||
|
assert trade is not None
|
||||||
|
assert trade.stake_amount == 0.0000098751
|
||||||
|
assert trade.is_open
|
||||||
|
assert trade.open_date is not None
|
||||||
|
|
||||||
|
assert Trade.total_open_trades_stakes() == 1.97502e-05
|
||||||
|
|
||||||
|
|
||||||
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
@ -423,11 +516,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
)
|
)
|
||||||
|
@ -455,7 +548,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
|
||||||
assert rate * amount >= default_conf['stake_amount']
|
assert rate * amount >= default_conf['stake_amount']
|
||||||
|
|
||||||
|
|
||||||
|
@ -499,7 +592,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
assert freqtrade.create_trade() is False
|
assert freqtrade.create_trade() is False
|
||||||
assert freqtrade._get_trade_stake_amount() is None
|
assert freqtrade._get_trade_stake_amount('ETH/BTC') is None
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
|
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
|
@ -598,7 +691,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||||
assert trade.amount == 90.99181073703367
|
assert trade.amount == 90.99181073703367
|
||||||
|
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Checking buy signals to create a new trade with stake_amount: 0.001000 ...',
|
'Buy signal found: about create a new trade with stake_amount: 0.001000 ...',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -668,6 +761,52 @@ def test_process_trade_handling(
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_trade_no_whitelist_pair(
|
||||||
|
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
|
||||||
|
""" Test _process with trade not in pair list """
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_markets=markets,
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
|
get_fee=fee,
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
pair = 'NOCLUE/BTC'
|
||||||
|
# create open trade not in whitelist
|
||||||
|
Trade.session.add(Trade(
|
||||||
|
pair=pair,
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
|
amount=20,
|
||||||
|
open_rate=0.01,
|
||||||
|
exchange='bittrex',
|
||||||
|
))
|
||||||
|
Trade.session.add(Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
is_open=True,
|
||||||
|
amount=12,
|
||||||
|
open_rate=0.001,
|
||||||
|
exchange='bittrex',
|
||||||
|
))
|
||||||
|
|
||||||
|
assert pair not in freqtrade.active_pair_whitelist
|
||||||
|
result = freqtrade._process()
|
||||||
|
assert pair in freqtrade.active_pair_whitelist
|
||||||
|
# Make sure each pair is only in the list once
|
||||||
|
assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist))
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker, default_conf) -> None:
|
def test_balance_fully_ask_side(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
@ -689,6 +828,192 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
||||||
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
stake_amount = 2
|
||||||
|
bid = 0.11
|
||||||
|
get_bid = MagicMock(return_value=bid)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
get_target_bid=get_bid,
|
||||||
|
_get_min_pair_stake_amount=MagicMock(return_value=1)
|
||||||
|
)
|
||||||
|
buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=buy_mm,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
print(buy_mm.call_args_list)
|
||||||
|
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
assert get_bid.call_count == 1
|
||||||
|
assert buy_mm.call_count == 1
|
||||||
|
call_args = buy_mm.call_args_list[0][1]
|
||||||
|
assert call_args['pair'] == pair
|
||||||
|
assert call_args['rate'] == bid
|
||||||
|
assert call_args['amount'] == stake_amount / bid
|
||||||
|
|
||||||
|
# Should create an open trade with an open order id
|
||||||
|
# As the order is not fulfilled yet
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
assert trade.is_open is True
|
||||||
|
assert trade.open_order_id == limit_buy_order['id']
|
||||||
|
|
||||||
|
# Test calling with price
|
||||||
|
fix_price = 0.06
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount, fix_price)
|
||||||
|
# Make sure get_target_bid wasn't called again
|
||||||
|
assert get_bid.call_count == 1
|
||||||
|
|
||||||
|
assert buy_mm.call_count == 2
|
||||||
|
call_args = buy_mm.call_args_list[1][1]
|
||||||
|
assert call_args['pair'] == pair
|
||||||
|
assert call_args['rate'] == fix_price
|
||||||
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
|
# In case of closed order
|
||||||
|
limit_buy_order['status'] = 'closed'
|
||||||
|
limit_buy_order['price'] = 10
|
||||||
|
limit_buy_order['cost'] = 100
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[2]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.open_rate == 10
|
||||||
|
assert trade.stake_amount == 100
|
||||||
|
|
||||||
|
# In case of rejected or expired order and partially filled
|
||||||
|
limit_buy_order['status'] = 'expired'
|
||||||
|
limit_buy_order['amount'] = 90.99181073
|
||||||
|
limit_buy_order['filled'] = 80.99181073
|
||||||
|
limit_buy_order['remaining'] = 10.00
|
||||||
|
limit_buy_order['price'] = 0.5
|
||||||
|
limit_buy_order['cost'] = 40.495905365
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
trade = Trade.query.all()[3]
|
||||||
|
assert trade
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.open_rate == 0.5
|
||||||
|
assert trade.stake_amount == 40.495905365
|
||||||
|
|
||||||
|
# In case of the order is rejected and not filled at all
|
||||||
|
limit_buy_order['status'] = 'rejected'
|
||||||
|
limit_buy_order['amount'] = 90.99181073
|
||||||
|
limit_buy_order['filled'] = 0.0
|
||||||
|
limit_buy_order['remaining'] = 90.99181073
|
||||||
|
limit_buy_order['price'] = 0.5
|
||||||
|
limit_buy_order['cost'] = 0.0
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
|
||||||
|
assert not freqtrade.execute_buy(pair, stake_amount)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
||||||
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
|
return_value=limit_buy_order['amount'])
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
trade.is_open = True
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade.stoploss_order_id == '13434334'
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
assert trade.is_open is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||||
|
markets, limit_buy_order, limit_sell_order) -> None:
|
||||||
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets,
|
||||||
|
stoploss_limit=stoploss_limit
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# First case: when stoploss is not yet set but the order is open
|
||||||
|
# should get the stoploss order id immediately
|
||||||
|
# and should return false as no trade actually happened
|
||||||
|
trade = MagicMock()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss_limit.call_count == 1
|
||||||
|
assert trade.stoploss_order_id == "13434334"
|
||||||
|
|
||||||
|
# Second case: when stoploss is set but it is not yet hit
|
||||||
|
# should do nothing and return false
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
|
||||||
|
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order)
|
||||||
|
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert trade.stoploss_order_id == 100
|
||||||
|
|
||||||
|
# Third case: when stoploss is set and it is hit
|
||||||
|
# should unset stoploss_order_id and return true
|
||||||
|
# as a trade actually happened
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.stoploss_order_id = 100
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
stoploss_order_hit = MagicMock(return_value={
|
||||||
|
'status': 'closed',
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 3,
|
||||||
|
'average': 2
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||||
|
assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog.record_tuples)
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.is_open is False
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
@ -804,7 +1129,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
assert trade.close_rate == 0.00001173
|
assert trade.close_rate == 0.00001173
|
||||||
assert trade.close_profit == 0.06201057
|
assert trade.close_profit == 0.06201058
|
||||||
assert trade.calc_profit() == 0.00006217
|
assert trade.calc_profit() == 0.00006217
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
|
||||||
|
@ -825,7 +1150,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order,
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade, value=(True, True))
|
patch_get_signal(freqtrade, value=(True, True))
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -881,7 +1206,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade, value=(True, False))
|
patch_get_signal(freqtrade, value=(True, False))
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -914,7 +1239,7 @@ def test_handle_trade_experimental(
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
@ -1231,9 +1556,10 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
'profit_percent': 0.06110514,
|
'profit_percent': 0.0611052,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.ROI.value
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
@ -1277,12 +1603,192 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
'profit_percent': -0.05478343,
|
'profit_percent': -0.05478342,
|
||||||
'stake_currency': 'BTC',
|
'stake_currency': 'BTC',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
|
||||||
|
ticker_sell_down,
|
||||||
|
markets, mocker) -> None:
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Decrease the price and sell it
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker_sell_down
|
||||||
|
)
|
||||||
|
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
# Setting trade stoploss to 0.01
|
||||||
|
|
||||||
|
trade.stop_loss = 0.00001099 * 0.99
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||||
|
sell_reason=SellType.STOP_LOSS)
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
assert {
|
||||||
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'limit': 1.08801e-05,
|
||||||
|
'amount': 90.99181073703367,
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'current_rate': 1.044e-05,
|
||||||
|
'profit_amount': -1.498e-05,
|
||||||
|
'profit_percent': -0.01493766,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD',
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
|
|
||||||
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
||||||
|
ticker, fee, ticker_sell_up,
|
||||||
|
markets, mocker) -> None:
|
||||||
|
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={
|
||||||
|
'id': 123,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
cancel_order = MagicMock(return_value=True)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellType.SELL_SIGNAL)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
assert cancel_order.call_count == 1
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
|
||||||
|
ticker, fee,
|
||||||
|
limit_buy_order,
|
||||||
|
markets, mocker) -> None:
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
_load_markets=MagicMock(return_value={}),
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
|
||||||
|
stoploss_limit = MagicMock(return_value={
|
||||||
|
'id': 123,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade
|
||||||
|
assert trade.stoploss_order_id == '123'
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
|
||||||
|
# Assuming stoploss on exchnage is hit
|
||||||
|
# stoploss_order_id should become None
|
||||||
|
# and trade should be sold at the price of stoploss
|
||||||
|
stoploss_limit_executed = MagicMock(return_value={
|
||||||
|
"id": "123",
|
||||||
|
"timestamp": 1542707426845,
|
||||||
|
"datetime": "2018-11-20T09:50:26.845Z",
|
||||||
|
"lastTradeTimestamp": None,
|
||||||
|
"symbol": "BTC/USDT",
|
||||||
|
"type": "stop_loss_limit",
|
||||||
|
"side": "sell",
|
||||||
|
"price": 1.08801,
|
||||||
|
"amount": 90.99181074,
|
||||||
|
"cost": 99.0000000032274,
|
||||||
|
"average": 1.08801,
|
||||||
|
"filled": 90.99181074,
|
||||||
|
"remaining": 0.0,
|
||||||
|
"status": "closed",
|
||||||
|
"fee": None,
|
||||||
|
"trades": None
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed)
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sell(trade)
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
|
assert trade.is_open is False
|
||||||
|
print(trade.sell_reason)
|
||||||
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
assert rpc_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
ticker_sell_up, markets, mocker) -> None:
|
ticker_sell_up, markets, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
@ -1324,7 +1830,9 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.172e-05,
|
'current_rate': 1.172e-05,
|
||||||
'profit_amount': 6.126e-05,
|
'profit_amount': 6.126e-05,
|
||||||
'profit_percent': 0.06110514,
|
'profit_percent': 0.0611052,
|
||||||
|
'sell_reason': SellType.ROI.value
|
||||||
|
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
@ -1370,7 +1878,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||||
'open_rate': 1.099e-05,
|
'open_rate': 1.099e-05,
|
||||||
'current_rate': 1.044e-05,
|
'current_rate': 1.044e-05,
|
||||||
'profit_amount': -5.492e-05,
|
'profit_amount': -5.492e-05,
|
||||||
'profit_percent': -0.05478343,
|
'profit_percent': -0.05478342,
|
||||||
|
'sell_reason': SellType.STOP_LOSS.value
|
||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
|
@ -1395,7 +1904,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -1427,7 +1936,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
@ -1458,7 +1967,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.stop_loss_reached = \
|
freqtrade.strategy.stop_loss_reached = \
|
||||||
lambda current_rate, trade, current_time, current_profit: SellCheckTuple(
|
lambda current_rate, trade, current_time, force_stoploss, current_profit: SellCheckTuple(
|
||||||
sell_flag=False, sell_type=SellType.NONE)
|
sell_flag=False, sell_type=SellType.NONE)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -1489,7 +1998,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -1519,7 +2028,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -1551,7 +2060,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog,
|
||||||
default_conf['trailing_stop'] = True
|
default_conf['trailing_stop'] = True
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -1586,7 +2095,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets
|
||||||
default_conf['trailing_stop_positive'] = 0.01
|
default_conf['trailing_stop_positive'] = 0.01
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
@ -1646,7 +2155,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
|
||||||
default_conf['trailing_stop_positive_offset'] = 0.011
|
default_conf['trailing_stop_positive_offset'] = 0.011
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
@ -1705,7 +2214,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
||||||
}
|
}
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
|
||||||
|
|
||||||
freqtrade.create_trade()
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
@ -2107,6 +2616,9 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
|
||||||
|
|
||||||
|
|
||||||
def test_startup_messages(default_conf, mocker):
|
def test_startup_messages(default_conf, mocker):
|
||||||
default_conf['dynamic_whitelist'] = 20
|
default_conf['pairlist'] = {'method': 'VolumePairList',
|
||||||
|
'config': {'number_assets': 20}
|
||||||
|
}
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
assert freqtrade.state is State.RUNNING
|
assert freqtrade.state is State.RUNNING
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
import datetime
|
import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
||||||
file_dump_json, format_ms_time, shorten_date)
|
file_dump_json, format_ms_time, shorten_date)
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.data.history import load_tickerdata_file
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@ def test_shorten_date() -> None:
|
||||||
assert shorten_date(str_data) == str_shorten_data
|
assert shorten_date(str_data) == str_shorten_data
|
||||||
|
|
||||||
|
|
||||||
def test_datesarray_to_datetimearray(ticker_history):
|
def test_datesarray_to_datetimearray(ticker_history_list):
|
||||||
dataframes = parse_ticker_dataframe(ticker_history)
|
dataframes = parse_ticker_dataframe(ticker_history_list)
|
||||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
assert isinstance(dates[0], datetime.datetime)
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
|
@ -34,7 +34,7 @@ def test_datesarray_to_datetimearray(ticker_history):
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
strategy = DefaultStrategy(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick)}
|
||||||
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
dates = common_datearray(dataframes)
|
dates = common_datearray(dataframes)
|
||||||
|
|
|
@ -62,7 +62,7 @@ def test_init_dryrun_db(default_conf, mocker):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
|
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
|
||||||
"""
|
"""
|
||||||
On this test we will buy and sell a crypto currency.
|
On this test we will buy and sell a crypto currency.
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
id=2,
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
|
@ -108,13 +109,53 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
|
||||||
assert trade.open_rate == 0.00001099
|
assert trade.open_rate == 0.00001099
|
||||||
assert trade.close_profit is None
|
assert trade.close_profit is None
|
||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
|
assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, "
|
||||||
|
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.close_rate == 0.00001173
|
assert trade.close_rate == 0.00001173
|
||||||
assert trade.close_profit == 0.06201057
|
assert trade.close_profit == 0.06201058
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, "
|
||||||
|
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
|
||||||
|
trade = Trade(
|
||||||
|
id=1,
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
)
|
||||||
|
|
||||||
|
trade.open_order_id = 'something'
|
||||||
|
trade.update(market_buy_order)
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.open_rate == 0.00004099
|
||||||
|
assert trade.close_profit is None
|
||||||
|
assert trade.close_date is None
|
||||||
|
assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, "
|
||||||
|
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
trade.open_order_id = 'something'
|
||||||
|
trade.update(market_sell_order)
|
||||||
|
assert trade.open_order_id is None
|
||||||
|
assert trade.close_rate == 0.00004173
|
||||||
|
assert trade.close_profit == 0.01297561
|
||||||
|
assert trade.close_date is not None
|
||||||
|
assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, "
|
||||||
|
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -129,16 +170,16 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||||
|
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
assert trade.calc_open_trade_price() == 0.001002500
|
assert trade.calc_open_trade_price() == 0.0010024999999225068
|
||||||
|
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_close_trade_price() == 0.0010646656
|
assert trade.calc_close_trade_price() == 0.0010646656050132426
|
||||||
|
|
||||||
# Profit in BTC
|
# Profit in BTC
|
||||||
assert trade.calc_profit() == 0.00006217
|
assert trade.calc_profit() == 0.00006217
|
||||||
|
|
||||||
# Profit in percent
|
# Profit in percent
|
||||||
assert trade.calc_profit_percent() == 0.06201057
|
assert trade.calc_profit_percent() == 0.06201058
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -207,10 +248,10 @@ def test_calc_open_trade_price(limit_buy_order, fee):
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get the open rate price with the standard fee rate
|
# Get the open rate price with the standard fee rate
|
||||||
assert trade.calc_open_trade_price() == 0.001002500
|
assert trade.calc_open_trade_price() == 0.0010024999999225068
|
||||||
|
|
||||||
# Get the open rate price with a custom fee rate
|
# Get the open rate price with a custom fee rate
|
||||||
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
|
assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -226,14 +267,14 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get the close rate price with a custom close rate and a regular fee rate
|
# Get the close rate price with a custom close rate and a regular fee rate
|
||||||
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318
|
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318470471794
|
||||||
|
|
||||||
# Get the close rate price with a custom close rate and a custom fee rate
|
# Get the close rate price with a custom close rate and a custom fee rate
|
||||||
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704
|
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704275749754
|
||||||
|
|
||||||
# Test when we apply a Sell order, and ask price with a custom fee rate
|
# Test when we apply a Sell order, and ask price with a custom fee rate
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
|
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972701635854
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -281,17 +322,17 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
|
||||||
trade.update(limit_buy_order) # Buy @ 0.00001099
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
# Get percent of profit with a custom rate (Higher than open rate)
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387
|
assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875
|
||||||
|
|
||||||
# Get percent of profit with a custom rate (Lower than open rate)
|
# Get percent of profit with a custom rate (Lower than open rate)
|
||||||
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
|
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828
|
||||||
|
|
||||||
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_profit_percent() == 0.06201057
|
assert trade.calc_profit_percent() == 0.06201058
|
||||||
|
|
||||||
# Test with a custom fee rate on the close trade
|
# Test with a custom fee rate on the close trade
|
||||||
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
assert trade.calc_profit_percent(fee=0.003) == 0.06147824
|
||||||
|
|
||||||
|
|
||||||
def test_clean_dry_run_db(default_conf, fee):
|
def test_clean_dry_run_db(default_conf, fee):
|
||||||
|
@ -426,6 +467,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
max_rate FLOAT,
|
max_rate FLOAT,
|
||||||
sell_reason VARCHAR,
|
sell_reason VARCHAR,
|
||||||
strategy VARCHAR,
|
strategy VARCHAR,
|
||||||
|
ticker_interval INTEGER,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);"""
|
);"""
|
||||||
|
@ -445,6 +487,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
|
|
||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
|
engine.execute("create index ix_trades_is_open on trades(is_open)")
|
||||||
|
engine.execute("create index ix_trades_pair on trades(pair)")
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
|
|
||||||
# fake previous backup
|
# fake previous backup
|
||||||
|
@ -471,6 +515,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
assert trade.sell_reason is None
|
assert trade.sell_reason is None
|
||||||
assert trade.strategy is None
|
assert trade.strategy is None
|
||||||
assert trade.ticker_interval is None
|
assert trade.ticker_interval is None
|
||||||
|
assert trade.stoploss_order_id is None
|
||||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
assert log_has("Running database migration - backup available as trades_bak2",
|
assert log_has("Running database migration - backup available as trades_bak2",
|
||||||
|
|
91
freqtrade/tests/test_wallets.py
Normal file
91
freqtrade/tests/test_wallets.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_wallet_at_boot(mocker, default_conf):
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value={
|
||||||
|
"BNT": {
|
||||||
|
"free": 1.0,
|
||||||
|
"used": 2.0,
|
||||||
|
"total": 3.0
|
||||||
|
},
|
||||||
|
"GAS": {
|
||||||
|
"free": 0.260739,
|
||||||
|
"used": 0.0,
|
||||||
|
"total": 0.260739
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
assert len(freqtrade.wallets.wallets) == 2
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].free == 1.0
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].used == 2.0
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].total == 3.0
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].free == 0.260739
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].used == 0.0
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].total == 0.260739
|
||||||
|
assert freqtrade.wallets.get_free('BNT') == 1.0
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value={
|
||||||
|
"BNT": {
|
||||||
|
"free": 1.2,
|
||||||
|
"used": 1.9,
|
||||||
|
"total": 3.5
|
||||||
|
},
|
||||||
|
"GAS": {
|
||||||
|
"free": 0.270739,
|
||||||
|
"used": 0.1,
|
||||||
|
"total": 0.260439
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade.wallets.update()
|
||||||
|
|
||||||
|
assert len(freqtrade.wallets.wallets) == 2
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].free == 1.2
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].used == 1.9
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].total == 3.5
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].free == 0.270739
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].used == 0.1
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].total == 0.260439
|
||||||
|
assert freqtrade.wallets.get_free('GAS') == 0.270739
|
||||||
|
assert freqtrade.wallets.get_used('GAS') == 0.1
|
||||||
|
assert freqtrade.wallets.get_total('GAS') == 0.260439
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_wallet_missing_data(mocker, default_conf):
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_balances=MagicMock(return_value={
|
||||||
|
"BNT": {
|
||||||
|
"free": 1.0,
|
||||||
|
"used": 2.0,
|
||||||
|
"total": 3.0
|
||||||
|
},
|
||||||
|
"GAS": {
|
||||||
|
"free": 0.260739,
|
||||||
|
"total": 0.260739
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
assert len(freqtrade.wallets.wallets) == 2
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].free == 1.0
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].used == 2.0
|
||||||
|
assert freqtrade.wallets.wallets['BNT'].total == 3.0
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].free == 0.260739
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].used is None
|
||||||
|
assert freqtrade.wallets.wallets['GAS'].total == 0.260739
|
||||||
|
assert freqtrade.wallets.get_free('GAS') == 0.260739
|
77
freqtrade/wallets.py
Normal file
77
freqtrade/wallets.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# pragma pylint: disable=W0603
|
||||||
|
""" Wallet """
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, NamedTuple
|
||||||
|
from collections import namedtuple
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Wallet(NamedTuple):
|
||||||
|
exchange: str
|
||||||
|
currency: str
|
||||||
|
free: float = 0
|
||||||
|
used: float = 0
|
||||||
|
total: float = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Wallets(object):
|
||||||
|
|
||||||
|
# wallet data structure
|
||||||
|
wallet = namedtuple(
|
||||||
|
'wallet',
|
||||||
|
['exchange', 'currency', 'free', 'used', 'total']
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, exchange: Exchange) -> None:
|
||||||
|
self.exchange = exchange
|
||||||
|
self.wallets: Dict[str, Any] = {}
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def get_free(self, currency) -> float:
|
||||||
|
|
||||||
|
if self.exchange._conf['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
balance = self.wallets.get(currency)
|
||||||
|
if balance and balance.free:
|
||||||
|
return balance.free
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_used(self, currency) -> float:
|
||||||
|
|
||||||
|
if self.exchange._conf['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
balance = self.wallets.get(currency)
|
||||||
|
if balance and balance.used:
|
||||||
|
return balance.used
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_total(self, currency) -> float:
|
||||||
|
|
||||||
|
if self.exchange._conf['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
balance = self.wallets.get(currency)
|
||||||
|
if balance and balance.total:
|
||||||
|
return balance.total
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
balances = self.exchange.get_balances()
|
||||||
|
|
||||||
|
for currency in balances:
|
||||||
|
self.wallets[currency] = Wallet(
|
||||||
|
self.exchange.id,
|
||||||
|
currency,
|
||||||
|
balances[currency].get('free', None),
|
||||||
|
balances[currency].get('used', None),
|
||||||
|
balances[currency].get('total', None)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('Wallets synced ...')
|
|
@ -1,7 +0,0 @@
|
||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
|
||||||
cd ta-lib && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
|
|
||||||
else
|
|
||||||
echo "TA-lib already installed, skipping download and build."
|
|
||||||
cd ta-lib && sudo make install && cd ..
|
|
||||||
fi
|
|
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Include all requirements to run the bot.
|
||||||
|
-r requirements.txt
|
||||||
|
|
||||||
|
flake8==3.6.0
|
||||||
|
pytest==4.0.2
|
||||||
|
pytest-mock==1.10.0
|
||||||
|
pytest-asyncio==0.9.0
|
||||||
|
pytest-cov==2.6.0
|
|
@ -1,26 +1,26 @@
|
||||||
ccxt==1.17.363
|
ccxt==1.18.71
|
||||||
SQLAlchemy==1.2.12
|
SQLAlchemy==1.2.15
|
||||||
python-telegram-bot==11.1.0
|
python-telegram-bot==11.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
cachetools==2.1.0
|
cachetools==3.0.0
|
||||||
requests==2.19.1
|
requests==2.21.0
|
||||||
urllib3==1.22
|
urllib3==1.24.1
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.23.4
|
pandas==0.23.4
|
||||||
scikit-learn==0.20.0
|
scikit-learn==0.20.2
|
||||||
scipy==1.1.0
|
joblib==0.13.0
|
||||||
|
scipy==1.2.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.15.2
|
numpy==1.15.4
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
pytest==3.8.1
|
|
||||||
pytest-mock==1.10.0
|
|
||||||
pytest-asyncio==0.9.0
|
|
||||||
pytest-cov==2.6.0
|
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scikit-optimize==0.5.2
|
scikit-optimize==0.5.2
|
||||||
|
|
||||||
# Required for plotting data
|
# find first, C search in arrays
|
||||||
#plotly==3.1.1
|
py_find_1st==1.1.3
|
||||||
|
|
||||||
|
#Load ticker files 30% faster
|
||||||
|
ujson==1.35
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Script to display when the bot will buy a specific pair
|
|
||||||
|
|
||||||
Mandatory Cli parameters:
|
|
||||||
-p / --pair: pair to examine
|
|
||||||
|
|
||||||
Optional Cli parameters
|
|
||||||
-d / --datadir: path to pair backtest data
|
|
||||||
--timerange: specify what timerange of data to use.
|
|
||||||
-l / --live: Live, to download the latest ticker for the pair
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from argparse import Namespace
|
|
||||||
from os import path
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
from typing import List, Dict
|
|
||||||
import gzip
|
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
|
||||||
from freqtrade import misc, constants
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
import dateutil.parser
|
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
|
||||||
|
|
||||||
|
|
||||||
def load_old_file(filename) -> (List[Dict], bool):
|
|
||||||
if not path.isfile(filename):
|
|
||||||
logger.warning("filename %s does not exist", filename)
|
|
||||||
return (None, False)
|
|
||||||
logger.debug('Loading ticker data from file %s', filename)
|
|
||||||
|
|
||||||
pairdata = None
|
|
||||||
|
|
||||||
if filename.endswith('.gz'):
|
|
||||||
logger.debug('Loading ticker data from file %s', filename)
|
|
||||||
is_zip = True
|
|
||||||
with gzip.open(filename) as tickerdata:
|
|
||||||
pairdata = json.load(tickerdata)
|
|
||||||
else:
|
|
||||||
is_zip = False
|
|
||||||
with open(filename) as tickerdata:
|
|
||||||
pairdata = json.load(tickerdata)
|
|
||||||
return (pairdata, is_zip)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_old_backtest_data(ticker) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Reads old backtest data
|
|
||||||
Format: "O": 8.794e-05,
|
|
||||||
"H": 8.948e-05,
|
|
||||||
"L": 8.794e-05,
|
|
||||||
"C": 8.88e-05,
|
|
||||||
"V": 991.09056638,
|
|
||||||
"T": "2017-11-26T08:50:00",
|
|
||||||
"BV": 0.0877869
|
|
||||||
"""
|
|
||||||
|
|
||||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open',
|
|
||||||
'H': 'high', 'L': 'low', 'T': 'date'}
|
|
||||||
|
|
||||||
frame = DataFrame(ticker) \
|
|
||||||
.rename(columns=columns)
|
|
||||||
if 'BV' in frame:
|
|
||||||
frame.drop('BV', 1, inplace=True)
|
|
||||||
if 'date' not in frame:
|
|
||||||
logger.warning("Date not in frame - probably not a Ticker file")
|
|
||||||
return None
|
|
||||||
frame.sort_values('date', inplace=True)
|
|
||||||
return frame
|
|
||||||
|
|
||||||
|
|
||||||
def convert_dataframe(frame: DataFrame):
|
|
||||||
"""Convert dataframe to new format"""
|
|
||||||
# reorder columns:
|
|
||||||
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|
||||||
frame = frame[cols]
|
|
||||||
|
|
||||||
# Make sure parsing/printing data is assumed to be UTC
|
|
||||||
frame['date'] = frame['date'].apply(
|
|
||||||
lambda d: int(dateutil.parser.parse(d+'+00:00').timestamp()) * 1000)
|
|
||||||
frame['date'] = frame['date'].astype('int64')
|
|
||||||
# Convert columns one by one to preserve type.
|
|
||||||
by_column = [frame[x].values.tolist() for x in frame.columns]
|
|
||||||
return list(list(x) for x in zip(*by_column))
|
|
||||||
|
|
||||||
|
|
||||||
def convert_file(filename: str, filename_new: str) -> None:
|
|
||||||
"""Converts a file from old format to ccxt format"""
|
|
||||||
(pairdata, is_zip) = load_old_file(filename)
|
|
||||||
if pairdata and type(pairdata) is list:
|
|
||||||
if type(pairdata[0]) is list:
|
|
||||||
logger.error("pairdata for %s already in new format", filename)
|
|
||||||
return
|
|
||||||
|
|
||||||
frame = parse_old_backtest_data(pairdata)
|
|
||||||
# Convert frame to new format
|
|
||||||
if frame is not None:
|
|
||||||
frame1 = convert_dataframe(frame)
|
|
||||||
misc.file_dump_json(filename_new, frame1, is_zip)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_main(args: Namespace) -> None:
|
|
||||||
"""
|
|
||||||
converts a folder given in --datadir from old to new format to support ccxt
|
|
||||||
"""
|
|
||||||
|
|
||||||
workdir = path.join(args.datadir, "")
|
|
||||||
logger.info("Workdir: %s", workdir)
|
|
||||||
|
|
||||||
for filename in glob.glob(workdir + "*.json"):
|
|
||||||
# swap currency names
|
|
||||||
ret = re.search(r'[A-Z_]{7,}', path.basename(filename))
|
|
||||||
if args.norename:
|
|
||||||
filename_new = filename
|
|
||||||
else:
|
|
||||||
if not ret:
|
|
||||||
logger.warning("file %s could not be converted, could not extract currencies",
|
|
||||||
filename)
|
|
||||||
continue
|
|
||||||
pair = ret.group(0)
|
|
||||||
currencies = pair.split("_")
|
|
||||||
if len(currencies) != 2:
|
|
||||||
logger.warning("file %s could not be converted, could not extract currencies",
|
|
||||||
filename)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ret_integer = re.search(r'\d+(?=\.json)', path.basename(filename))
|
|
||||||
ret_string = re.search(r'(\d+[mhdw])(?=\.json)', path.basename(filename))
|
|
||||||
|
|
||||||
if ret_integer:
|
|
||||||
minutes = int(ret_integer.group(0))
|
|
||||||
# default to adding 'm' to end of minutes for new interval name
|
|
||||||
interval = str(minutes) + 'm'
|
|
||||||
# but check if there is a mapping between int and string also
|
|
||||||
for str_interval, minutes_interval in constants.TICKER_INTERVAL_MINUTES.items():
|
|
||||||
if minutes_interval == minutes:
|
|
||||||
interval = str_interval
|
|
||||||
break
|
|
||||||
# change order on pairs if old ticker interval found
|
|
||||||
|
|
||||||
filename_new = path.join(path.dirname(filename),
|
|
||||||
f"{currencies[1]}_{currencies[0]}-{interval}.json")
|
|
||||||
|
|
||||||
elif ret_string:
|
|
||||||
interval = ret_string.group(0)
|
|
||||||
filename_new = path.join(path.dirname(filename),
|
|
||||||
f"{currencies[0]}_{currencies[1]}-{interval}.json")
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.warning("file %s could not be converted, interval not found", filename)
|
|
||||||
continue
|
|
||||||
|
|
||||||
logger.debug("Converting and renaming %s to %s", filename, filename_new)
|
|
||||||
convert_file(filename, filename_new)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_parse_args(args: List[str]) -> Namespace:
|
|
||||||
"""
|
|
||||||
Parse args passed to the script
|
|
||||||
:param args: Cli arguments
|
|
||||||
:return: args: Array with all arguments
|
|
||||||
"""
|
|
||||||
arguments = Arguments(args, 'Convert datafiles')
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'-d', '--datadir',
|
|
||||||
help='path to backtest data (default: %(default)s',
|
|
||||||
dest='datadir',
|
|
||||||
default=path.join('freqtrade', 'tests', 'testdata'),
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'-n', '--norename',
|
|
||||||
help='don''t rename files from BTC_<PAIR> to <PAIR>_BTC - '
|
|
||||||
'Note that not renaming will overwrite source files',
|
|
||||||
dest='norename',
|
|
||||||
default=False,
|
|
||||||
action='store_true'
|
|
||||||
)
|
|
||||||
|
|
||||||
return arguments.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
This function will initiate the bot and start the trading loop.
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
logger.info('Starting Dataframe conversation')
|
|
||||||
convert_main(convert_parse_args(sysargv))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main(sys.argv[1:])
|
|
|
@ -9,8 +9,15 @@ import arrow
|
||||||
from freqtrade import arguments
|
from freqtrade import arguments
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.optimize import download_backtesting_testdata
|
from freqtrade.data.history import download_pair_history
|
||||||
|
from freqtrade.configuration import Configuration, set_loggers
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
set_loggers(0)
|
||||||
|
|
||||||
DEFAULT_DL_PATH = 'user_data/data'
|
DEFAULT_DL_PATH = 'user_data/data'
|
||||||
|
|
||||||
|
@ -20,7 +27,29 @@ args = arguments.parse_args()
|
||||||
|
|
||||||
timeframes = args.timeframes
|
timeframes = args.timeframes
|
||||||
|
|
||||||
dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange)
|
if args.config:
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration._load_config_file(args.config)
|
||||||
|
|
||||||
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
else:
|
||||||
|
config = {'stake_currency': '',
|
||||||
|
'dry_run': True,
|
||||||
|
'exchange': {
|
||||||
|
'name': args.exchange,
|
||||||
|
'key': '',
|
||||||
|
'secret': '',
|
||||||
|
'pair_whitelist': [],
|
||||||
|
'ccxt_async_config': {
|
||||||
|
"enableRateLimit": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name'])
|
||||||
if args.export:
|
if args.export:
|
||||||
dl_path = Path(args.export)
|
dl_path = Path(args.export)
|
||||||
|
|
||||||
|
@ -45,18 +74,8 @@ if args.days:
|
||||||
|
|
||||||
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
||||||
|
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = Exchange({'key': '',
|
exchange = Exchange(config)
|
||||||
'secret': '',
|
|
||||||
'stake_currency': '',
|
|
||||||
'dry_run': True,
|
|
||||||
'exchange': {
|
|
||||||
'name': args.exchange,
|
|
||||||
'pair_whitelist': [],
|
|
||||||
'ccxt_rate_limit': False
|
|
||||||
}
|
|
||||||
})
|
|
||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
|
@ -73,7 +92,7 @@ for pair in PAIRS:
|
||||||
dl_file.unlink()
|
dl_file.unlink()
|
||||||
|
|
||||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||||
download_backtesting_testdata(str(dl_path), exchange=exchange,
|
download_pair_history(datadir=dl_path, exchange=exchange,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
tick_interval=tick_interval,
|
tick_interval=tick_interval,
|
||||||
timerange=timerange)
|
timerange=timerange)
|
||||||
|
|
|
@ -38,13 +38,13 @@ import pytz
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
|
||||||
from freqtrade import persistence
|
from freqtrade import persistence
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
from freqtrade.data import history
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.optimize.backtesting import setup_configuration
|
from freqtrade.optimize.backtesting import setup_configuration
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_CONF: Dict[str, Any] = {}
|
_CONF: Dict[str, Any] = {}
|
||||||
|
@ -139,10 +139,10 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pair.')
|
||||||
exchange.refresh_tickers([pair], tick_interval)
|
exchange.refresh_tickers([pair], tick_interval)
|
||||||
tickers[pair] = exchange.klines[pair]
|
tickers[pair] = exchange.klines(pair)
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(
|
tickers = history.load_data(
|
||||||
datadir=_CONF.get("datadir"),
|
datadir=Path(_CONF.get("datadir")),
|
||||||
pairs=[pair],
|
pairs=[pair],
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
refresh_pairs=_CONF.get('refresh_pairs', False),
|
||||||
|
|
|
@ -13,10 +13,10 @@ Optional Cli parameters
|
||||||
--export-filename: Specify where the backtest export is located.
|
--export-filename: Specify where the backtest export is located.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
@ -27,8 +27,8 @@ import plotly.graph_objs as go
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.data import history
|
||||||
import freqtrade.optimize as optimize
|
from freqtrade.resolvers import StrategyResolver
|
||||||
import freqtrade.misc as misc
|
import freqtrade.misc as misc
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,8 +120,8 @@ def plot_profit(args: Namespace) -> None:
|
||||||
pairs = list(set(pairs) & set(filter_pairs))
|
pairs = list(set(pairs) & set(filter_pairs))
|
||||||
logger.info('Filter, keep pairs %s' % pairs)
|
logger.info('Filter, keep pairs %s' % pairs)
|
||||||
|
|
||||||
tickers = optimize.load_data(
|
tickers = history.load_data(
|
||||||
datadir=config.get('datadir'),
|
datadir=Path(config.get('datadir')),
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=False,
|
refresh_pairs=False,
|
||||||
|
@ -187,7 +187,7 @@ def plot_profit(args: Namespace) -> None:
|
||||||
)
|
)
|
||||||
fig.append_trace(pair_profit, 3, 1)
|
fig.append_trace(pair_profit, 3, 1)
|
||||||
|
|
||||||
plot(fig, filename=os.path.join('user_data', 'freqtrade-profit-plot.html'))
|
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html')))
|
||||||
|
|
||||||
|
|
||||||
def define_index(min_date: int, max_date: int, interval: str) -> int:
|
def define_index(min_date: int, max_date: int, interval: str) -> int:
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -31,12 +31,15 @@ setup(name='freqtrade',
|
||||||
'pandas',
|
'pandas',
|
||||||
'scikit-learn',
|
'scikit-learn',
|
||||||
'scipy',
|
'scipy',
|
||||||
|
'joblib',
|
||||||
'jsonschema',
|
'jsonschema',
|
||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'coinmarketcap',
|
'coinmarketcap',
|
||||||
'scikit-optimize',
|
'scikit-optimize',
|
||||||
|
'ujson',
|
||||||
|
'py_find_1st'
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|
23
setup.sh
23
setup.sh
|
@ -28,6 +28,16 @@ function updateenv () {
|
||||||
pip3 install --quiet --upgrade pip
|
pip3 install --quiet --upgrade pip
|
||||||
pip3 install --quiet -r requirements.txt --upgrade
|
pip3 install --quiet -r requirements.txt --upgrade
|
||||||
pip3 install --quiet -r requirements.txt
|
pip3 install --quiet -r requirements.txt
|
||||||
|
|
||||||
|
read -p "Do you want to install dependencies for dev [Y/N]? "
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
pip3 install --quiet -r requirements-dev.txt --upgrade
|
||||||
|
pip3 install --quiet -r requirements-dev.txt
|
||||||
|
else
|
||||||
|
echo "Dev dependencies ignored."
|
||||||
|
fi
|
||||||
|
|
||||||
pip3 install --quiet -e .
|
pip3 install --quiet -e .
|
||||||
echo "pip3 install completed"
|
echo "pip3 install completed"
|
||||||
echo
|
echo
|
||||||
|
@ -35,10 +45,13 @@ function updateenv () {
|
||||||
|
|
||||||
# Install tab lib
|
# Install tab lib
|
||||||
function install_talib () {
|
function install_talib () {
|
||||||
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib && ./configure --prefix=/usr && make && sudo make install
|
cd ta-lib
|
||||||
cd .. && rm -rf ./ta-lib*
|
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||||
|
./configure --prefix=/usr/local
|
||||||
|
make
|
||||||
|
sudo make install
|
||||||
|
cd .. && rm -rf ./ta-lib/
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install bot MacOS
|
# Install bot MacOS
|
||||||
|
@ -50,8 +63,8 @@ function install_macos () {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
brew install python3 wget ta-lib
|
brew install python3 wget
|
||||||
|
install_talib
|
||||||
test_and_fix_python_on_mac
|
test_and_fix_python_on_mac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user