commit
1aa48f5f41
@ -3,3 +3,4 @@ omit =
|
|||||||
scripts/*
|
scripts/*
|
||||||
freqtrade/tests/*
|
freqtrade/tests/*
|
||||||
freqtrade/vendor/*
|
freqtrade/vendor/*
|
||||||
|
freqtrade/__main__.py
|
||||||
|
4
.flake8
Normal file
4
.flake8
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
ignore = E226,E302,E41,E126,F841
|
||||||
|
max-line-length = 160
|
||||||
|
exclude = */tests/*
|
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@ -6,10 +6,12 @@ If it hasn't been reported, please create a new issue.
|
|||||||
## Step 2: Describe your environment
|
## Step 2: Describe your environment
|
||||||
|
|
||||||
* Python Version: _____ (`python -V`)
|
* Python Version: _____ (`python -V`)
|
||||||
|
* CCXT version: _____ (`pip freeze | grep ccxt`)
|
||||||
* Branch: Master | Develop
|
* Branch: Master | Develop
|
||||||
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
|
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
|
||||||
|
|
||||||
## Step 3: Describe the problem:
|
## Step 3: Describe the problem:
|
||||||
|
|
||||||
*Explain the problem you have encountered*
|
*Explain the problem you have encountered*
|
||||||
|
|
||||||
### Steps to reproduce:
|
### Steps to reproduce:
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -6,7 +6,6 @@ config*.json
|
|||||||
.hyperopt
|
.hyperopt
|
||||||
logfile.txt
|
logfile.txt
|
||||||
hyperopt_trials.pickle
|
hyperopt_trials.pickle
|
||||||
user_data/
|
|
||||||
freqtrade-plot.html
|
freqtrade-plot.html
|
||||||
freqtrade-profit-plot.html
|
freqtrade-profit-plot.html
|
||||||
|
|
||||||
@ -27,8 +26,8 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
#lib/
|
||||||
lib64/
|
#lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
@ -42,6 +42,11 @@ pip3.6 install flake8 coveralls
|
|||||||
flake8 freqtrade
|
flake8 freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We receive a lot of code that fails the `flake8` checks.
|
||||||
|
To help with that, we encourage you to install the git pre-commit
|
||||||
|
hook that will warn you when you try to commit code that fails these checks.
|
||||||
|
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
|
||||||
|
|
||||||
## 3. Test if all type-hints are correct
|
## 3. Test if all type-hints are correct
|
||||||
|
|
||||||
**Install packages** (If not already installed)
|
**Install packages** (If not already installed)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
FROM python:3.6.5-slim-stretch
|
FROM python:3.6.5-slim-stretch
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
RUN apt-get update && apt-get -y install curl build-essential git && apt-get clean
|
||||||
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
||||||
tar xzvf - && \
|
tar xzvf - && \
|
||||||
cd ta-lib && \
|
cd ta-lib && \
|
||||||
|
85
README.md
85
README.md
@ -1,10 +1,20 @@
|
|||||||
# freqtrade
|
# freqtrade
|
||||||
|
|
||||||
[](https://travis-ci.org/freqtrade/freqtrade)
|
[](https://travis-ci.org/freqtrade/freqtrade)
|
||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://coveralls.io/github/berlinguyinca/freqtrade?branch=wohlgemuth)
|
||||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[](https://codeclimate.com/github/berlinguyinca/freqtrade/maintainability)
|
||||||
|
|
||||||
|
|
||||||
|
## First of all, this is a fork!
|
||||||
|
|
||||||
|
Basically I required a lot more features than the awesome default freqtrade version has to offer and since pull requests always take longer than exspected or the standard disagreements. I decided to maintain on main branch for my changes, called wohlgemuth, which is incidentally my last name and have a ton of little branches, with added features.
|
||||||
|
|
||||||
|
This basically allows people to use my version, or to easily merge changes into their forks or make PR's against the main repo, which is the best of both works.
|
||||||
|
|
||||||
|
This reminds of the Torvalds kernel vs the Cox kernel...
|
||||||
|
|
||||||
|
## Back to what this is actually about
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies designed to
|
Simple High frequency trading bot for crypto currencies designed to
|
||||||
support multi exchanges and be controlled via Telegram.
|
support multi exchanges and be controlled via Telegram.
|
||||||
|
|
||||||
@ -25,12 +35,12 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
- [Documentations](docs/index.md)
|
||||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
- [Installation](docs/installation.md)
|
||||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
- [Configuration](docs/configuration.md)
|
||||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Strategy Optimization](docs/bot-optimization.md)
|
||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Backtesting](docs/backtesting.md)
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Hyperopt](docs/hyperopt.md)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Help](#help--slack)
|
- [Help](#help--slack)
|
||||||
- [Bugs](#bugs--issues)
|
- [Bugs](#bugs--issues)
|
||||||
@ -44,11 +54,8 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
- [Software requirements](#software-requirements)
|
- [Software requirements](#software-requirements)
|
||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
The project is currently setup in two main branches:
|
|
||||||
- `develop` - This branch has often new features, but might also cause
|
if you like to use this fork, I highly recommend to utilize the 'wohlgemuth' branch, since this is the most stable. It will be synced against the original development branch and be enriched with all my changes.
|
||||||
breaking changes.
|
|
||||||
- `master` - This branch contains the latest stable release. The bot
|
|
||||||
'should' be stable on this branch, and is generally well tested.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
||||||
@ -65,6 +72,30 @@ strategy parameters with real exchange data.
|
|||||||
- [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.
|
||||||
|
|
||||||
|
### Additional features in this branch
|
||||||
|
|
||||||
|
#### Strategy:
|
||||||
|
|
||||||
|
- [x] loading strategies from Base64 encoded data in the config file
|
||||||
|
- [x] loading strategies from urls
|
||||||
|
- [x] trailing stop loss
|
||||||
|
|
||||||
|
#### Others:
|
||||||
|
|
||||||
|
- [x] more indicators
|
||||||
|
- [x] more telegram features
|
||||||
|
- [x] advanced plotting
|
||||||
|
- [x] [using book orders for buy and/or sell](docs/configuration.md)
|
||||||
|
- [x] [separated unfilled orders timeout](docs/configuration.md)
|
||||||
|
- [x] [option to disable buying](docs/configuration.md)
|
||||||
|
- [x] [option to get a buy price based on %](docs/configuration.md)
|
||||||
|
|
||||||
|
### Drawbacks
|
||||||
|
|
||||||
|
- [x] not as good documentation
|
||||||
|
- [x] maybe a bug here or there I haven't fixed yet
|
||||||
|
|
||||||
|
|
||||||
### Exchange marketplaces supported
|
### Exchange marketplaces supported
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/)
|
||||||
@ -73,7 +104,7 @@ strategy parameters with real exchange data.
|
|||||||
## Quick start
|
## Quick start
|
||||||
This quick start section is a very short explanation on how to test the
|
This quick start section is a very short explanation on how to test the
|
||||||
bot in dry-run. We invite you to read the
|
bot in dry-run. We invite you to read the
|
||||||
[bot documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
[bot documentation](docs/index.md)
|
||||||
to ensure you understand how the bot is working.
|
to ensure you understand how the bot is working.
|
||||||
|
|
||||||
### Easy installation
|
### Easy installation
|
||||||
@ -109,26 +140,26 @@ For any questions not covered by the documentation or for further
|
|||||||
information about the bot, we encourage you to join our slack channel.
|
information about the bot, we encourage you to join our slack channel.
|
||||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||||
|
|
||||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](issues?q=is%3Aissue)
|
||||||
If you discover a bug in the bot, please
|
If you discover a bug in the bot, please
|
||||||
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
[search our issue tracker](issues?q=is%3Aissue)
|
||||||
first. If it hasn't been reported, please
|
first. If it hasn't been reported, please
|
||||||
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
|
[create a new issue](issues/new) and
|
||||||
ensure you follow the template guide so that our team can assist you as
|
ensure you follow the template guide so that our team can assist you as
|
||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
### [Feature Requests](labels/enhancement)
|
||||||
Have you a great idea to improve the bot you want to share? Please,
|
Have you a great idea to improve the bot you want to share? Please,
|
||||||
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
first search if this feature was not [already discussed](labels/enhancement).
|
||||||
If it hasn't been requested, please
|
If it hasn't been requested, please
|
||||||
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
|
[create a new request](issues/new)
|
||||||
and ensure you follow the template guide so that it does not get lost
|
and ensure you follow the template guide so that it does not get lost
|
||||||
in the bug reports.
|
in the bug reports.
|
||||||
|
|
||||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
### [Pull Requests](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](develop/CONTRIBUTING.md)
|
||||||
to understand the requirements before sending your pull-requests.
|
to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
**Important:** Always create your PR against the `develop` branch, not
|
**Important:** Always create your PR against the `develop` branch, not
|
||||||
@ -171,14 +202,14 @@ optional arguments:
|
|||||||
only if dry_run is enabled.
|
only if dry_run is enabled.
|
||||||
```
|
```
|
||||||
More details on:
|
More details on:
|
||||||
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- [How to run the bot](docs/bot-usage.md#bot-commands)
|
||||||
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
- [How to use Backtesting](docs/bot-usage.md#backtesting-commands)
|
||||||
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
- [How to use Hyperopt](docs/bot-usage.md#hyperopt-commands)
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
Telegram is not mandatory. However, this is a great way to control your
|
Telegram is not mandatory. However, this is a great way to control your
|
||||||
bot. More details on our
|
bot. More details on our
|
||||||
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
[documentation](develop/docs/index.md)
|
||||||
|
|
||||||
- `/start`: Starts the trader
|
- `/start`: Starts the trader
|
||||||
- `/stop`: Stops the trader
|
- `/stop`: Stops the trader
|
||||||
|
@ -5,11 +5,24 @@
|
|||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"unfilledtimeout": 600,
|
"disable_buy" : false,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"buy":10,
|
||||||
|
"sell":30
|
||||||
|
}
|
||||||
|
"trailing_stop": {
|
||||||
|
"positive" : 0.005
|
||||||
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
"ask_last_balance": 0.0,
|
||||||
"use_book_order": true,
|
"use_book_order": false,
|
||||||
"book_order_top": 6
|
"book_order_top": 6,
|
||||||
|
"percent_from_top": 0
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_book_order": false,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 30
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -33,7 +46,8 @@
|
|||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
"sell_profit_only": false
|
"sell_profit_only": false,
|
||||||
|
"sell_fullfilled_at_roi": false
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
|
"disable_buy" : false,
|
||||||
"ticker_interval": "5m",
|
"ticker_interval": "5m",
|
||||||
|
"trailing_stop": true,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
@ -12,11 +14,20 @@
|
|||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": {
|
||||||
|
"buy":10,
|
||||||
|
"sell":30
|
||||||
|
}
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
"ask_last_balance": 0.0,
|
||||||
"use_book_order": true,
|
"use_book_order": false,
|
||||||
"book_order_top": 6
|
"book_order_top": 6,
|
||||||
|
"percent_from_top": 0
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_book_order": false,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 30
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -40,7 +51,8 @@
|
|||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
"sell_profit_only": false
|
"sell_profit_only": false,
|
||||||
|
"sell_fullfilled_at_roi": false
|
||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
# Backtesting
|
# Backtesting
|
||||||
|
|
||||||
This page explains how to validate your strategy performance by using
|
This page explains how to validate your strategy performance by using
|
||||||
Backtesting.
|
Backtesting.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
|
||||||
- [Understand the backtesting result](#understand-the-backtesting-result)
|
- [Understand the backtesting result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Test your strategy with Backtesting
|
## Test your strategy with Backtesting
|
||||||
|
|
||||||
Now you have good Buy and Sell strategies, you want to test it against
|
Now you have good Buy and Sell strategies, you want to test it against
|
||||||
real data. This is what we call
|
real data. This is what we call
|
||||||
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
|
||||||
|
|
||||||
|
|
||||||
Backtesting will use the crypto-currencies (pair) from your config file
|
Backtesting will use the crypto-currencies (pair) from your config file
|
||||||
and load static tickers located in
|
and load static tickers located in
|
||||||
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
|
||||||
@ -19,70 +21,80 @@ If the 5 min and 1 min ticker for the crypto-currencies to test is not
|
|||||||
already in the `testdata` folder, backtesting will download them
|
already in the `testdata` folder, backtesting will download them
|
||||||
automatically. Testdata files will not be updated until you specify it.
|
automatically. Testdata files will not be updated until you specify it.
|
||||||
|
|
||||||
The result of backtesting will confirm you if your bot as more chance to
|
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
|
||||||
make a profit than a loss.
|
|
||||||
|
|
||||||
|
|
||||||
The backtesting is very easy with freqtrade.
|
The backtesting is very easy with freqtrade.
|
||||||
|
|
||||||
### Run a backtesting against the currencies listed in your config file
|
### Run a backtesting against the currencies listed in your config file
|
||||||
**With 5 min tickers (Per default)**
|
#### With 5 min tickers (Per default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
||||||
```
|
```
|
||||||
|
|
||||||
**With 1 min tickers**
|
#### With 1 min tickers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
**Update cached pairs with the latest data**
|
#### Update cached pairs with the latest data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
||||||
```
|
```
|
||||||
|
|
||||||
**With live data (do not alter your testdata files)**
|
#### With live data (do not alter your testdata files)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||||
```
|
```
|
||||||
|
|
||||||
**Using a different on-disk ticker-data source**
|
#### Using a different on-disk ticker-data source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
||||||
```
|
```
|
||||||
|
|
||||||
**With a (custom) strategy file**
|
#### With a (custom) strategy file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
python3 ./freqtrade/main.py -s TestStrategy backtesting
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
|
||||||
|
|
||||||
**Exporting trades to file**
|
#### Exporting trades to file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
**Exporting trades to file specifying a custom filename**
|
#### Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Running backtest with smaller testset
|
||||||
|
|
||||||
**Running backtest with smaller testset**
|
|
||||||
Use the `--timerange` argument to change how much of the testset
|
Use the `--timerange` argument to change how much of the testset
|
||||||
you want to use. The last N ticks/timeframes will be used.
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --timerange=-200
|
python3 ./freqtrade/main.py backtesting --timerange=-200
|
||||||
```
|
```
|
||||||
|
|
||||||
***Advanced use of timerange***
|
#### Advanced use of timerange
|
||||||
|
|
||||||
Doing `--timerange=-200` will get the last 200 timeframes
|
Doing `--timerange=-200` will get the last 200 timeframes
|
||||||
from your inputdata. You can also specify specific dates,
|
from your inputdata. You can also specify specific dates,
|
||||||
or a range span indexed by start and stop.
|
or a range span indexed by start and stop.
|
||||||
|
|
||||||
The full timerange specification:
|
The full timerange specification:
|
||||||
|
|
||||||
- Use last 123 tickframes of data: `--timerange=-123`
|
- Use last 123 tickframes of data: `--timerange=-123`
|
||||||
- Use first 123 tickframes of data: `--timerange=123-`
|
- Use first 123 tickframes of data: `--timerange=123-`
|
||||||
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||||
@ -92,11 +104,12 @@ The full timerange specification:
|
|||||||
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
- Use tickframes between POSIX timestamps 1527595200 1527618600:
|
||||||
`--timerange=1527595200-1527618600`
|
`--timerange=1527595200-1527618600`
|
||||||
|
|
||||||
|
#### Downloading new set of ticker data
|
||||||
|
|
||||||
**Downloading new set of ticker data**
|
|
||||||
To download new set of backtesting ticker data, you can use a download script.
|
To download new set of backtesting ticker data, you can use a download script.
|
||||||
|
|
||||||
If you are using Binance for example:
|
If you are using Binance for example:
|
||||||
|
|
||||||
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
|
||||||
- update the `pairs.json` to contain the currency pairs you are interested in.
|
- update the `pairs.json` to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
@ -119,33 +132,55 @@ This will download ticker data for all the currency pairs you defined in `pairs.
|
|||||||
- To download ticker data for only 10 days, use `--days 10`.
|
- To download ticker data for only 10 days, use `--days 10`.
|
||||||
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
|
||||||
|
|
||||||
|
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
|
||||||
For help about backtesting usage, please refer to
|
|
||||||
[Backtesting commands](#backtesting-commands).
|
|
||||||
|
|
||||||
## Understand the backtesting result
|
## Understand the backtesting result
|
||||||
|
|
||||||
The most important in the backtesting is to understand the result.
|
The most important in the backtesting is to understand the result.
|
||||||
|
|
||||||
A backtesting result will look like that:
|
A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
====================== BACKTESTING REPORT ================================
|
======================================== BACKTESTING REPORT =========================================
|
||||||
pair buy count avg profit % total profit BTC avg duration
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
-------- ----------- -------------- ------------------ --------------
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
ETH/BTC 56 -0.67 -0.00075455 62.3
|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
|
||||||
LTC/BTC 38 -0.48 -0.00036315 57.9
|
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
|
||||||
ETC/BTC 42 -1.15 -0.00096469 67.0
|
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
|
||||||
DASH/BTC 72 -0.62 -0.00089368 39.9
|
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
|
||||||
ZEC/BTC 45 -0.46 -0.00041387 63.2
|
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
|
||||||
XLM/BTC 24 -0.88 -0.00041846 47.7
|
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
|
||||||
NXT/BTC 24 0.68 0.00031833 40.2
|
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
|
||||||
POWR/BTC 35 0.98 0.00064887 45.3
|
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
|
||||||
ADA/BTC 43 -0.39 -0.00032292 55.0
|
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
|
||||||
XMR/BTC 40 -0.40 -0.00032181 47.4
|
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
|
||||||
|
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
|
||||||
|
====================================== LEFT OPEN TRADES REPORT ======================================
|
||||||
|
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|
||||||
|
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
|
||||||
|
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
|
||||||
|
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
|
||||||
|
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
|
||||||
|
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
|
||||||
|
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
|
||||||
|
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
|
||||||
|
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
|
||||||
|
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The 1st table will contain all trades the bot made.
|
||||||
|
|
||||||
|
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
|
||||||
|
These trades are also included in the first table, but are extracted separately for clarity.
|
||||||
|
|
||||||
The last line will give you the overall performance of your strategy,
|
The last line will give you the overall performance of your strategy,
|
||||||
here:
|
here:
|
||||||
|
|
||||||
```
|
```
|
||||||
TOTAL 419 -0.41 -0.00348593 52.9
|
TOTAL 419 -0.41 -0.00348593 52.9
|
||||||
```
|
```
|
||||||
@ -161,6 +196,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
|
|||||||
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
|
||||||
expect the bot to make more profit than 1% (because it will sell every
|
expect the bot to make more profit than 1% (because it will sell every
|
||||||
time a trade will reach 1%).
|
time a trade will reach 1%).
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"0": 0.01
|
"0": 0.01
|
||||||
@ -173,6 +209,7 @@ profit. Hence, keep in mind that your performance is a mix of your
|
|||||||
strategies, your configuration, and the crypto-currency you have set up.
|
strategies, your configuration, and the crypto-currency you have set up.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
optimal parameters to use for your strategy?
|
optimal parameters to use for your strategy?
|
||||||
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
@ -160,13 +160,12 @@ the parameter `-l` or `--live`.
|
|||||||
|
|
||||||
## Hyperopt commands
|
## Hyperopt commands
|
||||||
|
|
||||||
It is possible to use hyperopt for trading strategy optimization.
|
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||||
Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
|
to find optimal parameter values for your stategy.
|
||||||
located in `freqtrade/optimize/hyperopt_conf.py`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
||||||
[--timerange TIMERANGE] [-e INT] [--use-mongodb]
|
[--timerange TIMERANGE] [-e INT]
|
||||||
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
@ -176,11 +175,8 @@ optional arguments:
|
|||||||
--realistic-simulation
|
--realistic-simulation
|
||||||
uses max_open_trades from config to simulate real
|
uses max_open_trades from config to simulate real
|
||||||
world limitations
|
world limitations
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE specify what timerange of data to use.
|
||||||
specify what timerange of data to use.
|
|
||||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||||
--use-mongodb parallelize evaluations with mongodb (requires mongod
|
|
||||||
in PATH)
|
|
||||||
-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
|
||||||
list. Default: all
|
list. Default: all
|
||||||
|
@ -18,12 +18,20 @@ The table below will list all configuration parameters.
|
|||||||
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
||||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
||||||
| `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
|
||||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. [More information below](docs/configuration.md#what-are-the-valid-values-for-fiat_display_currency).
|
||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. [More information below](docs/configuration.md#switch-to-dry-run--paper-trading-mode)
|
||||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. [More information below](docs/configuration.md#understanding-minimal_roi).
|
||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
| `disable_buy` | false | No | Disables buying of crypto-currency. Bot will continue to sell.
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
||||||
|
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
||||||
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. [More information below](docs/configuration.md#understanding-bid_strategyask_last_balance).
|
||||||
|
| `bid_strategy.use_book_order` | false | No | Use book order to set the bidding price. [More information below](docs/configuration.md#understanding-bid_strategyuse_book_order).
|
||||||
|
| `bid_strategy.book_order_top` | 1 | No | Selects the top n bidding price in book order. [More information below](docs/configuration.md#understanding-bid_strategyuse_book_order).
|
||||||
|
| `bid_strategy.percent_from_top` | 0 | No | Set the percent to deduct from the buy rate from book order (if enabled) or from ask/last price. [More information below](docs/configuration.md#understanding-bid_strategypercent_from_top).
|
||||||
|
| `ask_strategy.use_book_order` | false | No | Use book order to set the asking price. More information below.
|
||||||
|
| `ask_strategy.book_order_min` | 1 | No | The minimum index from the top to search for profitable asking price from book order. [More information below](docs/configuration.md#understanding-ask_strategyuse_book_order).
|
||||||
|
| `ask_strategy.book_order_max` | 1 | No | The maximum index from the top to search for profitable asking price from book order. [More information below](docs/configuration.md#understanding-ask_strategyuse_book_order).
|
||||||
| `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.
|
||||||
@ -31,11 +39,12 @@ The table below will list all configuration parameters.
|
|||||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||||
|
| `experimental.sell_fullfilled_at_roi` | false | No | automatically creates a sell order based on `minimal_roi` once a buy order has been fullfilled.
|
||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
| `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](docs/configuration.md#understanding-initial_state).
|
||||||
| `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.
|
||||||
@ -43,7 +52,7 @@ The table below will list all configuration parameters.
|
|||||||
The definition of each config parameters is in
|
The definition of each config parameters is in
|
||||||
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understanding minimal_roi
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
@ -56,41 +65,34 @@ See the example below:
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
Most of the strategy files already include the optimal `minimal_roi`
|
Most of the strategy files already include the optimal `minimal_roi` value. This parameter is optional. If you use it, it will take over the `minimal_roi` value from the strategy file.
|
||||||
value. This parameter is optional. If you use it, it will take over the
|
|
||||||
`minimal_roi` value from the strategy file.
|
|
||||||
|
|
||||||
### Understand stoploss
|
### Understanding stoploss
|
||||||
`stoploss` is loss in percentage that should trigger a sale.
|
`stoploss` is loss in percentage that should trigger a sale. For example value `-0.10` will cause immediate sell if the
|
||||||
For example value `-0.10` will cause immediate sell if the
|
|
||||||
profit dips below -10% for a given trade. This parameter is optional.
|
profit dips below -10% for a given trade. This parameter is optional.
|
||||||
|
|
||||||
Most of the strategy files already include the optimal `stoploss`
|
Most of the strategy files already include the optimal `stoploss` value. This parameter is optional. If you use it, it will take over the `stoploss` value from the strategy file.
|
||||||
value. This parameter is optional. If you use it, it will take over the
|
|
||||||
`stoploss` value from the strategy file.
|
|
||||||
|
|
||||||
### Understand initial_state
|
### Understanding initial_state
|
||||||
`initial_state` is an optional field that defines the initial application state.
|
`initial_state` is an optional field that defines the initial application state. Possible values are `running` or `stopped`. (default=`running`) If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
Possible values are `running` or `stopped`. (default=`running`)
|
|
||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
|
||||||
|
|
||||||
### Understand process_throttle_secs
|
### Understanding 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 before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or the static list of pairs) if we should buy.
|
||||||
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
|
||||||
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
|
||||||
the static list of pairs) if we should buy.
|
|
||||||
|
|
||||||
### Understand ask_last_balance
|
### Understanding bid_strategy.ask_last_balance
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will use the `last` price and the values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary.
|
||||||
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
|
|
||||||
end up paying more then would probably have been necessary.
|
|
||||||
|
|
||||||
### What values for exchange.name?
|
### Understanding bid_strategy.use_book_order
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
`bid_strategy.use_book_order` loads the exchange book order and sets the bidding price between `book_order_min` and `book_order_max` value. If the `book_order_top` is set to 3, then the 3rd bidding price from the top of the book order will be selected as the bidding price for the trade.
|
||||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
|
||||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
### Understanding bid_strategy.percent_from_top
|
||||||
with only Bittrex and Binance.
|
`bid_strategy.percent_from_top` sets the percent to deduct from buy price of the pair. If `bid_strategy.use_book_order` is enabled, the percent value is deducted from the rate of `book_order_top`, otherwise, the percent value is deducted from the value provided by `bid_strategy.ask_last_balance`. Example: If `ask_last_balance` rate is 100 and the `bid_strategy.percent_from_top` is `0.005` or `0.5%`, the bot would buy at the price of `99.5`.
|
||||||
|
|
||||||
|
### Understanding ask_strategy.use_book_order
|
||||||
|
`ask_strategy.use_book_order` loads the exchange book order and sets the askng price based on the `book_order_top` value. If the `book_order_min` is set to 3 and `book_order_max` is set to 10, then the bot will search between top 3rd and 10th asking prices from the top of the book order will be selected as the bidding price for the trade.
|
||||||
|
|
||||||
|
### What are the valid values for exchange.name?
|
||||||
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115+ cryptocurrency exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was thoroughly tested with only Bittrex and Binance.
|
||||||
|
|
||||||
The bot was tested with the following exchanges:
|
The bot was tested with the following exchanges:
|
||||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||||
@ -98,13 +100,13 @@ The bot was tested with the following exchanges:
|
|||||||
|
|
||||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
|
|
||||||
### What values for fiat_display_currency?
|
### What are the valid values for fiat_display_currency?
|
||||||
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||||
In addition to central bank currencies, a range of cryto currencies are supported.
|
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||||
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run / paper trading mode
|
||||||
We recommend starting the bot in dry-run mode to see how your bot will
|
We recommend starting the bot in dry-run mode to see how your bot will
|
||||||
behave and how is the performance of your strategy. In Dry-run mode the
|
behave and how is the performance of your strategy. In Dry-run mode the
|
||||||
bot does not engage your money. It only runs a live simulation without
|
bot does not engage your money. It only runs a live simulation without
|
||||||
@ -131,7 +133,7 @@ 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.
|
||||||
|
|
||||||
## Switch to production mode
|
## Switch to production / live 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.
|
||||||
|
@ -9,7 +9,6 @@ parameters with Hyperopt.
|
|||||||
- [Advanced Hyperopt notions](#advanced-notions)
|
- [Advanced Hyperopt notions](#advanced-notions)
|
||||||
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
||||||
- [Execute Hyperopt](#execute-hyperopt)
|
- [Execute Hyperopt](#execute-hyperopt)
|
||||||
- [Hyperopt with MongoDB](#hyperopt-with-mongoDB)
|
|
||||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Prepare Hyperopt
|
## Prepare Hyperopt
|
||||||
@ -194,41 +193,6 @@ Legal values are:
|
|||||||
- `stoploss`: search for the best stoploss value
|
- `stoploss`: search for the best stoploss value
|
||||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
|
||||||
### Hyperopt with MongoDB
|
|
||||||
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
|
|
||||||
executing the previous command is the execution takes a long time.
|
|
||||||
To accelerate it you can use hyperopt with MongoDB.
|
|
||||||
|
|
||||||
To run hyperopt with MongoDb you will need 3 terminals.
|
|
||||||
|
|
||||||
**Terminal 1: Start MongoDB**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 scripts/start-mongodb.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 2: Start Hyperopt worker**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 scripts/start-hyperopt-worker.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Terminal 3: Start Hyperopt with MongoDB**
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
source .env/bin/activate
|
|
||||||
python3 ./freqtrade/main.py -c config.json hyperopt --use-mongodb
|
|
||||||
```
|
|
||||||
|
|
||||||
**Re-run an Hyperopt**
|
|
||||||
To re-run Hyperopt you have to delete the existing MongoDB table.
|
|
||||||
```bash
|
|
||||||
cd <freqtrade>
|
|
||||||
rm -rf .hyperopt/mongodb/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Understand the hyperopts result
|
## Understand the hyperopts result
|
||||||
Once Hyperopt is completed you can use the result to adding new buy
|
Once Hyperopt is completed you can use the result to adding new buy
|
||||||
signal. Given following result from hyperopt:
|
signal. Given following result from hyperopt:
|
||||||
|
@ -184,6 +184,26 @@ docker start freqtrade
|
|||||||
|
|
||||||
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### 7. Backtest with docker
|
||||||
|
|
||||||
|
The following assumes that the above steps (1-4) have been completed successfully.
|
||||||
|
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||||
|
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
|
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
|
||||||
|
freqtrade --strategy AwsomelyProfitableStrategy backtesting
|
||||||
|
```
|
||||||
|
|
||||||
|
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details.
|
||||||
|
|
||||||
|
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example).
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Custom Installation
|
## Custom Installation
|
||||||
@ -225,17 +245,7 @@ cd ..
|
|||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. [Optional] Install MongoDB
|
#### 3. Install FreqTrade
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install mongodb-org
|
|
||||||
```
|
|
||||||
|
|
||||||
> Complete tutorial from Digital Ocean: [How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04).
|
|
||||||
|
|
||||||
#### 4. Install FreqTrade
|
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
@ -243,7 +253,7 @@ Clone the git repository:
|
|||||||
git clone https://github.com/freqtrade/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 5. Configure `freqtrade` as a `systemd` service
|
#### 4. Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
|
||||||
@ -267,19 +277,7 @@ sudo loginctl enable-linger "$USER"
|
|||||||
brew install python3 git wget ta-lib
|
brew install python3 git wget ta-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. [Optional] Install MongoDB
|
#### 2. Install FreqTrade
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
|
|
||||||
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
|
|
||||||
mkdir -p <path_freqtrade>/env/mongodb
|
|
||||||
cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
|
|
||||||
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Install FreqTrade
|
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
|
50
docs/stoploss.md
Normal file
50
docs/stoploss.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Stop Loss support
|
||||||
|
|
||||||
|
at this stage the bot contains the following stoploss support modes:
|
||||||
|
|
||||||
|
1. static stop loss, defined in either the strategy or configuration
|
||||||
|
|
||||||
|
2. trailing stop loss, defined in the configuration
|
||||||
|
|
||||||
|
3. trailing stop loss, custom positive loss, defined in configuration
|
||||||
|
|
||||||
|
## Static Stop Loss
|
||||||
|
|
||||||
|
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
||||||
|
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
||||||
|
|
||||||
|
## Trail Stop Loss
|
||||||
|
|
||||||
|
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
||||||
|
To enable this Feauture all you have to do, is to define the configuration element:
|
||||||
|
|
||||||
|
```
|
||||||
|
"trailing_stop" : True
|
||||||
|
```
|
||||||
|
This will now actiave an algorithm, whihch automatically moves up your stop loss, every time the price of your asset increases.
|
||||||
|
|
||||||
|
For example, simplified math,
|
||||||
|
|
||||||
|
* you buy an asset at a price of 100$
|
||||||
|
* your stop loss is defined at 2%
|
||||||
|
* which means your stop loss, gets triggered once your asset dropped below 98$
|
||||||
|
* assuming your asset now increases in proce to 102$
|
||||||
|
* your stop loss, will now be 2% of 102$ or 99.96$
|
||||||
|
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
|
||||||
|
|
||||||
|
basically what this means, is that your stop loss will be adjusted to be always be 2% of the highest observed price
|
||||||
|
|
||||||
|
### Custom positive loss
|
||||||
|
|
||||||
|
due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive,
|
||||||
|
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the
|
||||||
|
black, it will be changed to be only a 1% stop loss
|
||||||
|
|
||||||
|
this can be configured in the main configuration file, the following way:
|
||||||
|
|
||||||
|
```
|
||||||
|
"trailing_stop": {
|
||||||
|
"positive" : 0.01
|
||||||
|
},
|
||||||
|
```
|
||||||
|
The 0.01 would translate to a 1% stop loss, once you hit profit.
|
@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | | Stops the trader
|
||||||
|
| `/reload_conf` | | Reloads the configuration file
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format
|
| `/status table` | | List all open trades in a table format
|
||||||
| `/count` | | Displays number of trades used and available
|
| `/count` | | Displays number of trades used and available
|
||||||
|
@ -7,14 +7,14 @@ from enum import Enum
|
|||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_fee, get_ticker_history
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ class Analyze(object):
|
|||||||
Analyze class contains everything the bot need to determine if the situation is good for
|
Analyze class contains everything the bot need to determine if the situation is good for
|
||||||
buying or selling.
|
buying or selling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Init Analyze
|
Init Analyze
|
||||||
@ -62,10 +63,10 @@ class Analyze(object):
|
|||||||
'close': 'last',
|
'close': 'last',
|
||||||
'volume': 'max',
|
'volume': 'max',
|
||||||
})
|
})
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, pair: str = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
@ -73,23 +74,23 @@ class Analyze(object):
|
|||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
you are using. Let uncomment only the indicator you are using in your strategies
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||||
"""
|
"""
|
||||||
return self.strategy.populate_indicators(dataframe=dataframe)
|
return self.strategy.advise_indicators(dataframe=dataframe, pair=pair)
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, pair: str = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
return self.strategy.populate_buy_trend(dataframe=dataframe)
|
return self.strategy.advise_buy(dataframe=dataframe, pair=pair)
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, pair: str = None) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
return self.strategy.advise_sell(dataframe=dataframe, pair=pair)
|
||||||
|
|
||||||
def get_ticker_interval(self) -> str:
|
def get_ticker_interval(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -98,16 +99,20 @@ class Analyze(object):
|
|||||||
"""
|
"""
|
||||||
return self.strategy.ticker_interval
|
return self.strategy.ticker_interval
|
||||||
|
|
||||||
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
def analyze_ticker(self, ticker_history: List[Dict], pair: str) -> 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 = self.parse_ticker_dataframe(ticker_history)
|
dataframe = self.parse_ticker_dataframe(ticker_history)
|
||||||
dataframe = self.populate_indicators(dataframe)
|
# eliminate partials for known exchanges that sends partial candles
|
||||||
dataframe = self.populate_buy_trend(dataframe)
|
if self.config['exchange']['name'] in ['binance']:
|
||||||
dataframe = self.populate_sell_trend(dataframe)
|
logger.info('eliminating partial candle')
|
||||||
|
dataframe.drop(dataframe.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
|
dataframe = self.populate_indicators(dataframe, pair)
|
||||||
|
dataframe = self.populate_buy_trend(dataframe, pair)
|
||||||
|
dataframe = self.populate_sell_trend(dataframe, pair)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]:
|
def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]:
|
||||||
@ -123,7 +128,7 @@ class Analyze(object):
|
|||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dataframe = self.analyze_ticker(ticker_hist)
|
dataframe = self.analyze_ticker(ticker_hist, 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',
|
||||||
@ -148,7 +153,8 @@ class Analyze(object):
|
|||||||
# Check if dataframe is out of date
|
# Check if dataframe is out of date
|
||||||
signal_date = arrow.get(latest['date'])
|
signal_date = arrow.get(latest['date'])
|
||||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
if signal_date < arrow.utcnow() - timedelta(minutes=(interval_minutes + 5)):
|
if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))):
|
||||||
|
logger.debug('signal %s vs arrow now %s', signal_date, arrow.utcnow())
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair,
|
pair,
|
||||||
@ -196,10 +202,40 @@ class Analyze(object):
|
|||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
if trade.stop_loss is None:
|
||||||
|
# initially adjust the stop loss to the base value
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss)
|
||||||
|
|
||||||
|
# evaluate if the stoploss was hit
|
||||||
|
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
|
||||||
|
|
||||||
|
if 'trailing_stop' in self.config and self.config['trailing_stop']:
|
||||||
|
logger.debug(
|
||||||
|
"HIT STOP: current price at {:.6f}, stop loss is {:.6f}, "
|
||||||
|
"initial stop loss was at {:.6f}, trade opened at {:.6f}".format(
|
||||||
|
current_rate, trade.stop_loss, trade.initial_stop_loss, trade.open_rate))
|
||||||
|
logger.debug("trailing stop saved us: {:.6f}"
|
||||||
|
.format(trade.stop_loss - trade.initial_stop_loss))
|
||||||
|
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# update the stop loss afterwards, after all by definition it's supposed to be hanging
|
||||||
|
if 'trailing_stop' in self.config and self.config['trailing_stop']:
|
||||||
|
|
||||||
|
# check if we have a special stop loss for positive condition
|
||||||
|
# and if profit is positive
|
||||||
|
stop_loss_value = self.strategy.stoploss
|
||||||
|
if isinstance(self.config['trailing_stop'], dict) and \
|
||||||
|
'positive' in self.config['trailing_stop'] and \
|
||||||
|
current_profit > 0:
|
||||||
|
|
||||||
|
logger.debug("using positive stop loss mode: {} since we have profit {}".format(
|
||||||
|
self.config['trailing_stop']['positive'], current_profit))
|
||||||
|
stop_loss_value = self.config['trailing_stop']['positive']
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
for duration, threshold in self.strategy.minimal_roi.items():
|
for duration, threshold in self.strategy.minimal_roi.items():
|
||||||
@ -216,3 +252,48 @@ class Analyze(object):
|
|||||||
"""
|
"""
|
||||||
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
||||||
for pair, pair_data in tickerdata.items()}
|
for pair, pair_data in tickerdata.items()}
|
||||||
|
|
||||||
|
def trunc_num(self, f, n):
|
||||||
|
import math
|
||||||
|
return math.floor(f * 10 ** n) / 10 ** n
|
||||||
|
|
||||||
|
def get_roi_rate(self, trade: Trade, sell_rate: float) -> float:
|
||||||
|
"""
|
||||||
|
Calculates sell rate based on roi
|
||||||
|
"""
|
||||||
|
current_time = datetime.utcnow()
|
||||||
|
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
|
for duration, threshold in self.strategy.minimal_roi.items():
|
||||||
|
if time_diff > duration:
|
||||||
|
roi_rate = self.trunc_num((trade.open_rate * (1 + threshold)) * (1+(2.1*get_fee(trade.pair))), 8)
|
||||||
|
logger.info('trying to selling at roi rate %0.8f', roi_rate)
|
||||||
|
return roi_rate
|
||||||
|
break
|
||||||
|
return sell_rate
|
||||||
|
|
||||||
|
def order_book_to_dataframe(data: list) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Gets order book list, returns dataframe with below format
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
bids b_size a_sum asks a_size a_sum
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
cols = ['bids', 'b_size']
|
||||||
|
bids_frame = DataFrame(data['bids'], columns=cols)
|
||||||
|
# add cumulative sum column
|
||||||
|
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
|
||||||
|
cols2 = ['asks', 'a_size']
|
||||||
|
asks_frame = DataFrame(data['asks'], columns=cols2)
|
||||||
|
# add cumulative sum column
|
||||||
|
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
|
||||||
|
|
||||||
|
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'], \
|
||||||
|
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1, \
|
||||||
|
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def order_book_dom() -> DataFrame:
|
||||||
|
# https://stackoverflow.com/questions/36835793/pandas-group-by-consecutive-ranges
|
||||||
|
return DataFrame
|
||||||
|
|
||||||
|
@ -203,12 +203,6 @@ class Arguments(object):
|
|||||||
type=int,
|
type=int,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
'--use-mongodb',
|
|
||||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
|
||||||
dest='mongodb',
|
|
||||||
action='store_true',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-s', '--spaces',
|
'-s', '--spaces',
|
||||||
help='Specify which parameters to hyperopt. Space separate list. \
|
help='Specify which parameters to hyperopt. Space separate list. \
|
||||||
@ -224,7 +218,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
|
||||||
|
|
||||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
@ -235,10 +229,14 @@ class Arguments(object):
|
|||||||
self.backtesting_options(backtesting_cmd)
|
self.backtesting_options(backtesting_cmd)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
try:
|
||||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
from freqtrade.optimize import hyperopt
|
||||||
self.optimizer_shared_options(hyperopt_cmd)
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||||
self.hyperopt_options(hyperopt_cmd)
|
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||||
|
self.optimizer_shared_options(hyperopt_cmd)
|
||||||
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
except ImportError as e:
|
||||||
|
logging.warn("no hyper opt found - skipping support for it")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_timerange(text: Optional[str]) -> TimeRange:
|
def parse_timerange(text: Optional[str]) -> TimeRange:
|
||||||
@ -295,6 +293,93 @@ class Arguments(object):
|
|||||||
default=None
|
default=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--stop-loss',
|
||||||
|
help='Renders stop/loss information in the main chart',
|
||||||
|
dest='stoplossdisplay',
|
||||||
|
action='store_true',
|
||||||
|
default=False
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-rsi',
|
||||||
|
help='Renders a rsi chart of the given RSI dataframe name, for example --plot-rsi rsi',
|
||||||
|
dest='plotrsi',
|
||||||
|
nargs='+',
|
||||||
|
default=None
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-cci',
|
||||||
|
help='Renders a cci chart of the given CCI dataframe name, for example --plot-cci cci',
|
||||||
|
dest='plotcci',
|
||||||
|
nargs='+',
|
||||||
|
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-osc',
|
||||||
|
help='Renders a osc chart of the given osc dataframe name, for example --plot-osc osc',
|
||||||
|
dest='plotosc',
|
||||||
|
nargs='+',
|
||||||
|
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-cmf',
|
||||||
|
help='Renders a cmf chart of the given cmf dataframe name, for example --plot-cmf cmf',
|
||||||
|
dest='plotcmf',
|
||||||
|
nargs='+',
|
||||||
|
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-macd',
|
||||||
|
help='Renders a macd chart of the given '
|
||||||
|
'dataframe name, for example --plot-macd macd',
|
||||||
|
dest='plotmacd',
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-dataframe',
|
||||||
|
help='Renders the specified dataframes',
|
||||||
|
dest='plotdataframe',
|
||||||
|
default=None,
|
||||||
|
nargs='+',
|
||||||
|
type=str
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-dataframe-marker',
|
||||||
|
help='Renders the specified dataframes as markers. '
|
||||||
|
'Accepted values for a marker are either 100 or -100',
|
||||||
|
dest='plotdataframemarker',
|
||||||
|
default=None,
|
||||||
|
nargs='+',
|
||||||
|
type=str
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-volume',
|
||||||
|
help='plots the volume as a sub plot',
|
||||||
|
dest='plotvolume',
|
||||||
|
action='store_true'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--plot-max-ticks',
|
||||||
|
help='specify an upper limit of how many ticks we can display',
|
||||||
|
dest='plotticks',
|
||||||
|
default=750,
|
||||||
|
type=int
|
||||||
|
)
|
||||||
|
|
||||||
def testdata_dl_options(self) -> None:
|
def testdata_dl_options(self) -> None:
|
||||||
"""
|
"""
|
||||||
Parses given arguments for testdata download
|
Parses given arguments for testdata download
|
||||||
|
@ -188,11 +188,6 @@ class Configuration(object):
|
|||||||
logger.info('Parameter --epochs detected ...')
|
logger.info('Parameter --epochs detected ...')
|
||||||
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
||||||
|
|
||||||
# If --mongodb is used we add it to the configuration
|
|
||||||
if 'mongodb' in self.args and self.args.mongodb:
|
|
||||||
config.update({'mongodb': self.args.mongodb})
|
|
||||||
logger.info('Parameter --use-mongodb detected ...')
|
|
||||||
|
|
||||||
# If --spaces is used we add it to the configuration
|
# If --spaces is used we add it to the configuration
|
||||||
if 'spaces' in self.args and self.args.spaces:
|
if 'spaces' in self.args and self.args.spaces:
|
||||||
config.update({'spaces': self.args.spaces})
|
config.update({'spaces': self.args.spaces})
|
||||||
|
@ -35,7 +35,7 @@ SUPPORTED_FIAT = [
|
|||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
CONF_SCHEMA = {
|
CONF_SCHEMA = {
|
||||||
@ -55,7 +55,14 @@ CONF_SCHEMA = {
|
|||||||
'minProperties': 1
|
'minProperties': 1
|
||||||
},
|
},
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
'unfilledtimeout': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'buy': {'type': 'number', 'minimum': 1},
|
||||||
|
'sell': {'type': 'number', 'minimum': 1}
|
||||||
|
},
|
||||||
|
'required': ['buy', 'sell']
|
||||||
|
},
|
||||||
'bid_strategy': {
|
'bid_strategy': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -66,16 +73,27 @@ CONF_SCHEMA = {
|
|||||||
'exclusiveMaximum': False
|
'exclusiveMaximum': False
|
||||||
},
|
},
|
||||||
'use_book_order': {'type': 'boolean'},
|
'use_book_order': {'type': 'boolean'},
|
||||||
'book_order_top': {'type': 'number', 'maximum':20,'minimum':1}
|
'book_order_top': {'type': 'number', 'maximum': 20, 'minimum': 1},
|
||||||
|
'percent_from_top': {'type': 'number', 'minimum': 0}
|
||||||
},
|
},
|
||||||
'required': ['ask_last_balance']
|
'required': ['ask_last_balance', 'use_book_order']
|
||||||
|
},
|
||||||
|
'ask_strategy': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'use_book_order': {'type': 'boolean'},
|
||||||
|
'book_order_min': {'type': 'number', 'minimum': 1},
|
||||||
|
'book_order_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
||||||
|
},
|
||||||
|
'required': ['use_book_order']
|
||||||
},
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
'use_sell_signal': {'type': 'boolean'},
|
||||||
'sell_profit_only': {'type': 'boolean'}
|
'sell_profit_only': {'type': 'boolean'},
|
||||||
|
'sell_fullfilled_at_roi': {'type': 'boolean'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
|
@ -45,6 +45,7 @@ def retrier(f):
|
|||||||
else:
|
else:
|
||||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@ -239,10 +240,21 @@ def get_balances() -> dict:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict:
|
def get_order_book(pair: str, limit: Optional[int] = 100) -> dict:
|
||||||
try:
|
try:
|
||||||
return _API.fetch_order_book(pair)
|
params = {}
|
||||||
|
# 20180619: bittrex doesnt support limits -.-
|
||||||
|
# 20180619: binance limit fix.. binance currently has valid range
|
||||||
|
if _API.name == 'Binance':
|
||||||
|
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||||
|
for limitx in limit_range:
|
||||||
|
if limit < limitx:
|
||||||
|
limit = limitx
|
||||||
|
break
|
||||||
|
|
||||||
|
return _API.fetch_l2_order_book(pair, limit)
|
||||||
except ccxt.NotSupported as e:
|
except ccxt.NotSupported as e:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {_API.name} does not support fetching order book.'
|
f'Exchange {_API.name} does not support fetching order book.'
|
||||||
@ -253,6 +265,7 @@ def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_tickers() -> Dict:
|
def get_tickers() -> Dict:
|
||||||
try:
|
try:
|
||||||
@ -297,8 +310,8 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
|
|||||||
try:
|
try:
|
||||||
# last item should be in the time interval [now - tick_interval, now]
|
# last item should be in the time interval [now - tick_interval, now]
|
||||||
till_time_ms = arrow.utcnow().shift(
|
till_time_ms = arrow.utcnow().shift(
|
||||||
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
|
||||||
).timestamp * 1000
|
).timestamp * 1000
|
||||||
# it looks as if some exchanges return cached data
|
# it looks as if some exchanges return cached data
|
||||||
# and they update it one in several minute, so 10 mins interval
|
# and they update it one in several minute, so 10 mins interval
|
||||||
# is necessary to skeep downloading of an empty array when all
|
# is necessary to skeep downloading of an empty array when all
|
||||||
|
@ -33,7 +33,7 @@ class FreqtradeBot(object):
|
|||||||
This is from here the bot start its logic.
|
This is from here the bot start its logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any])-> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Init all variables and object the bot need to work
|
Init all variables and object the bot need to work
|
||||||
:param config: configuration dict, you can use the Configuration.get_config()
|
:param config: configuration dict, you can use the Configuration.get_config()
|
||||||
@ -76,17 +76,14 @@ class FreqtradeBot(object):
|
|||||||
else:
|
else:
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
|
|
||||||
def clean(self) -> bool:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Cleanup the application state und finish all pending tasks
|
Cleanup pending resources on an already stopped bot
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.rpc.send_msg('*Status:* `Stopping trader...`')
|
logger.info('Cleaning up modules ...')
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
|
||||||
self.state = State.STOPPED
|
|
||||||
self.rpc.cleanup()
|
self.rpc.cleanup()
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
return True
|
|
||||||
|
|
||||||
def worker(self, old_state: State = None) -> State:
|
def worker(self, old_state: State = None) -> State:
|
||||||
"""
|
"""
|
||||||
@ -99,6 +96,12 @@ class FreqtradeBot(object):
|
|||||||
if state != old_state:
|
if state != old_state:
|
||||||
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
|
if (('use_book_order' in self.config['bid_strategy'] and \
|
||||||
|
self.config['bid_strategy'].get('use_book_order', False)) or \
|
||||||
|
('use_book_order' in self.config['ask_strategy'] and \
|
||||||
|
self.config['ask_strategy'].get('use_book_order', False))) and \
|
||||||
|
self.config['dry_run'] and state == State.RUNNING:
|
||||||
|
self.rpc.send_msg('*Warning:* `Order book enabled in dry run. Results will be misleading`')
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@ -159,13 +162,17 @@ class FreqtradeBot(object):
|
|||||||
state_changed |= self.process_maybe_execute_sell(trade)
|
state_changed |= self.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
# Then looking for buy opportunities
|
# Then looking for buy opportunities
|
||||||
if len(trades) < self.config['max_open_trades']:
|
if (self.config.get('disable_buy', False)):
|
||||||
state_changed = self.process_maybe_execute_buy()
|
logger.info('Buy disabled...')
|
||||||
|
else:
|
||||||
|
if len(trades) < self.config['max_open_trades']:
|
||||||
|
state_changed = self.process_maybe_execute_buy()
|
||||||
|
|
||||||
if 'unfilledtimeout' in self.config:
|
if 'unfilledtimeout' in self.config:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout(self.config['unfilledtimeout'])
|
if not self.config['dry_run']:
|
||||||
Trade.session.flush()
|
self.check_handle_timedout()
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
except TemporaryError as error:
|
except TemporaryError as error:
|
||||||
logger.warning('%s, retrying in 30 seconds...', error)
|
logger.warning('%s, retrying in 30 seconds...', error)
|
||||||
@ -243,19 +250,40 @@ class FreqtradeBot(object):
|
|||||||
:param ticker: Ticker to use for getting Ask and Last Price
|
:param ticker: Ticker to use for getting Ask and Last Price
|
||||||
:return: float: Price
|
:return: float: Price
|
||||||
"""
|
"""
|
||||||
|
ticker = exchange.get_ticker(pair)
|
||||||
|
logger.debug('ticker data %s', ticker)
|
||||||
|
|
||||||
if self.config['bid_strategy']['use_book_order']:
|
if ticker['ask'] < ticker['last']:
|
||||||
logger.info('Using order book ')
|
ticker_rate = ticker['ask']
|
||||||
orderBook = exchange.get_order_book(pair)
|
|
||||||
return orderBook['bids'][self.config['bid_strategy']['use_book_order']][0]
|
|
||||||
else:
|
else:
|
||||||
logger.info('Using Ask / Last Price')
|
|
||||||
ticker = exchange.get_ticker(pair);
|
|
||||||
if ticker['ask'] < ticker['last']:
|
|
||||||
return ticker['ask']
|
|
||||||
balance = self.config['bid_strategy']['ask_last_balance']
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
used_rate = ticker_rate
|
||||||
|
|
||||||
|
if 'use_book_order' in self.config['bid_strategy'] and self.config['bid_strategy'].get('use_book_order', False):
|
||||||
|
logger.info('Getting price from Order Book')
|
||||||
|
orderBook_top = self.config.get('bid_strategy', {}).get('book_order_top', 1)
|
||||||
|
orderBook = exchange.get_order_book(pair, orderBook_top)
|
||||||
|
# top 1 = index 0
|
||||||
|
orderBook_rate = orderBook['bids'][orderBook_top - 1][0]
|
||||||
|
orderBook_rate = orderBook_rate + 0.00000001
|
||||||
|
# if ticker has lower rate, then use ticker ( usefull if down trending )
|
||||||
|
logger.info('...book order buy rate %0.8f', orderBook_rate)
|
||||||
|
if ticker_rate < orderBook_rate:
|
||||||
|
logger.info('...using ticker rate instead %0.8f', ticker_rate)
|
||||||
|
used_rate = ticker_rate
|
||||||
|
used_rate = orderBook_rate
|
||||||
|
else:
|
||||||
|
logger.info('Using Last Ask / Last Price')
|
||||||
|
used_rate = ticker_rate
|
||||||
|
percent_from_top = self.config.get('bid_strategy', {}).get('percent_from_top', 0)
|
||||||
|
if percent_from_top > 0:
|
||||||
|
used_rate = used_rate - (used_rate * percent_from_top)
|
||||||
|
used_rate = self.analyze.trunc_num(used_rate, 8)
|
||||||
|
logger.info('...percent_from_top enabled, new buy rate %0.8f', used_rate)
|
||||||
|
|
||||||
|
return used_rate
|
||||||
|
|
||||||
def create_trade(self) -> bool:
|
def create_trade(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -264,6 +292,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
|
||||||
"""
|
"""
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
|
|
||||||
interval = self.analyze.get_ticker_interval()
|
interval = self.analyze.get_ticker_interval()
|
||||||
stake_currency = self.config['stake_currency']
|
stake_currency = self.config['stake_currency']
|
||||||
fiat_currency = self.config['fiat_display_currency']
|
fiat_currency = self.config['fiat_display_currency']
|
||||||
@ -274,8 +303,10 @@ class FreqtradeBot(object):
|
|||||||
stake_amount
|
stake_amount
|
||||||
)
|
)
|
||||||
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(stake_currency) < stake_amount:
|
current_balance = exchange.get_balance(self.config['stake_currency'])
|
||||||
|
if current_balance < stake_amount:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f'stake amount is not fulfilled (currency={stake_currency})')
|
f'stake amount is not fulfilled (currency={stake_currency})')
|
||||||
|
|
||||||
@ -431,33 +462,68 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
raise ValueError(f'attempt to handle closed trade: {trade}')
|
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.info('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
sell_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
logger.info(' ticker rate %0.8f', sell_rate)
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
|
|
||||||
if self.config.get('experimental', {}).get('use_sell_signal'):
|
if self.config.get('experimental', {}).get('use_sell_signal'):
|
||||||
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
|
||||||
|
|
||||||
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
is_set_fullfilled_at_roi = self.config.get('experimental', {}).get('sell_fullfilled_at_roi', False)
|
||||||
self.execute_sell(trade, current_rate)
|
if is_set_fullfilled_at_roi:
|
||||||
return True
|
sell_rate = self.analyze.get_roi_rate(trade, sell_rate)
|
||||||
|
|
||||||
|
if 'ask_strategy' in self.config and self.config['ask_strategy'].get('use_book_order', False):
|
||||||
|
logger.info('Using order book for selling...')
|
||||||
|
# logger.debug('Order book %s',orderBook)
|
||||||
|
orderBook_min = self.config['ask_strategy'].get('book_order_min', 1)
|
||||||
|
orderBook_max = self.config['ask_strategy'].get('book_order_max', 1)
|
||||||
|
|
||||||
|
orderBook = exchange.get_order_book(trade.pair, orderBook_max)
|
||||||
|
|
||||||
|
for i in range(orderBook_min, orderBook_max + 1):
|
||||||
|
orderBook_rate = orderBook['asks'][i - 1][0]
|
||||||
|
|
||||||
|
# if orderbook has higher rate (high profit),
|
||||||
|
# use orderbook, otherwise just use bids rate
|
||||||
|
logger.info(' order book asks top %s: %0.8f', i, orderBook_rate)
|
||||||
|
if sell_rate < orderBook_rate:
|
||||||
|
sell_rate = orderBook_rate
|
||||||
|
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
|
return True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info('checking sell')
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
|
return True
|
||||||
|
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
|
if self.analyze.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell):
|
||||||
|
self.execute_sell(trade, sell_rate)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_handle_timedout(self) -> None:
|
||||||
"""
|
"""
|
||||||
Check if any orders are timed out and cancel if neccessary
|
Check if any orders are timed out and cancel if neccessary
|
||||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
buy_timeout = self.config['unfilledtimeout']['buy']
|
||||||
|
sell_timeout = self.config['unfilledtimeout']['sell']
|
||||||
|
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||||
|
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||||
|
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
try:
|
try:
|
||||||
# FIXME: Somehow the query above returns results
|
# FIXME: Somehow the query above returns results
|
||||||
# where the open_order_id is in fact None.
|
# where the open_order_id is in fact None.
|
||||||
# This is probably because the record got
|
# This is probably because the record get_trades_for_order
|
||||||
# updated via /forcesell in a different thread.
|
# updated via /forcesell in a different thread.
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
continue
|
continue
|
||||||
@ -471,13 +537,11 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
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 order['status'] == 'open':
|
||||||
continue
|
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
if order['side'] == 'buy' and ordertime < timeoutthreashold:
|
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
||||||
self.handle_timedout_limit_buy(trade, order)
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
|
|
||||||
self.handle_timedout_limit_sell(trade, order)
|
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
# it is conditionally called in the
|
# it is conditionally called in the
|
||||||
@ -568,7 +632,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
fiat
|
fiat
|
||||||
)
|
)
|
||||||
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
||||||
f'` / {profit_fiat:.3f} {fiat})`'\
|
f'` / {profit_fiat:.3f} {fiat})`' \
|
||||||
''
|
''
|
||||||
# Because telegram._forcesell does not have the configuration
|
# Because telegram._forcesell does not have the configuration
|
||||||
# Ignore the FIAT value and does not show the stake_currency as well
|
# Ignore the FIAT value and does not show the stake_currency as well
|
||||||
|
@ -5,12 +5,14 @@ Read the documentation to know what cli arguments you need.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from argparse import Namespace
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
@ -44,6 +46,8 @@ def main(sysargv: List[str]) -> None:
|
|||||||
state = None
|
state = None
|
||||||
while 1:
|
while 1:
|
||||||
state = freqtrade.worker(old_state=state)
|
state = freqtrade.worker(old_state=state)
|
||||||
|
if state == State.RELOAD_CONF:
|
||||||
|
freqtrade = reconfigure(freqtrade, args)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('SIGINT received, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
@ -55,10 +59,28 @@ def main(sysargv: List[str]) -> None:
|
|||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
if freqtrade:
|
if freqtrade:
|
||||||
freqtrade.clean()
|
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
|
||||||
|
freqtrade.cleanup()
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
Cleans up current instance, reloads the configuration and returns the new instance
|
||||||
|
"""
|
||||||
|
# Clean up current modules
|
||||||
|
freqtrade.cleanup()
|
||||||
|
|
||||||
|
# Create new instance
|
||||||
|
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
||||||
|
freqtrade.rpc.send_msg(
|
||||||
|
'*Status:* `Config reloaded ...`'.format(
|
||||||
|
freqtrade.state.name.lower()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return freqtrade
|
||||||
|
|
||||||
|
|
||||||
def set_loggers() -> None:
|
def set_loggers() -> None:
|
||||||
"""
|
"""
|
||||||
Set the logger level for Third party libs
|
Set the logger level for Third party libs
|
||||||
|
@ -71,7 +71,6 @@ def file_dump_json(filename, data, is_zip=False) -> None:
|
|||||||
:param data: JSON Data to save
|
:param data: JSON Data to save
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
print(f'dumping json to "{filename}"')
|
|
||||||
|
|
||||||
if is_zip:
|
if is_zip:
|
||||||
if not filename.endswith('.gz'):
|
if not filename.endswith('.gz'):
|
||||||
|
@ -11,8 +11,6 @@ from freqtrade import misc, constants
|
|||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
|
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +81,7 @@ def load_tickerdata_file(
|
|||||||
|
|
||||||
def load_data(datadir: str,
|
def load_data(datadir: str,
|
||||||
ticker_interval: str,
|
ticker_interval: str,
|
||||||
pairs: Optional[List[str]] = None,
|
pairs: List[str],
|
||||||
refresh_pairs: Optional[bool] = False,
|
refresh_pairs: Optional[bool] = False,
|
||||||
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
@ -92,14 +90,12 @@ def load_data(datadir: str,
|
|||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
# If the user force the refresh of pairs
|
# If the user force the refresh of pairs
|
||||||
if refresh_pairs:
|
if refresh_pairs:
|
||||||
logger.info('Download data for all pairs and store them in %s', datadir)
|
logger.info('Download data for all pairs and store them in %s', datadir)
|
||||||
download_pairs(datadir, _pairs, ticker_interval, timerange=timerange)
|
download_pairs(datadir, pairs, ticker_interval, timerange=timerange)
|
||||||
|
|
||||||
for pair in _pairs:
|
for pair in pairs:
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
if pairdata:
|
if pairdata:
|
||||||
result[pair] = pairdata
|
result[pair] = pairdata
|
||||||
|
@ -6,7 +6,8 @@ This module contains the backtesting logic
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, Tuple, Any, List, Optional
|
from datetime import datetime
|
||||||
|
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -23,6 +24,21 @@ from freqtrade.persistence import Trade
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BacktestResult(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple Defining BacktestResults inputs.
|
||||||
|
"""
|
||||||
|
pair: str
|
||||||
|
profit_percent: float
|
||||||
|
profit_abs: float
|
||||||
|
open_time: datetime
|
||||||
|
close_time: datetime
|
||||||
|
open_index: int
|
||||||
|
close_index: int
|
||||||
|
trade_duration: float
|
||||||
|
open_at_end: bool
|
||||||
|
|
||||||
|
|
||||||
class Backtesting(object):
|
class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
Backtesting class, this class contains all the logic to run a backtest
|
Backtesting class, this class contains all the logic to run a backtest
|
||||||
@ -31,6 +47,7 @@ class Backtesting(object):
|
|||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = Analyze(self.config)
|
self.analyze = Analyze(self.config)
|
||||||
@ -58,47 +75,63 @@ class Backtesting(object):
|
|||||||
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
|
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
|
||||||
for frame in data.values()
|
for frame in data.values()
|
||||||
]
|
]
|
||||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
return min(timeframe, key=operator.itemgetter(0))[0], max(timeframe, key=operator.itemgetter(1))[1]
|
||||||
max(timeframe, key=operator.itemgetter(1))[1]
|
|
||||||
|
|
||||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
"""
|
"""
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
:return: pretty printed table with tabulate as str
|
:return: pretty printed table with tabulate as str
|
||||||
"""
|
"""
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
floatfmt, headers, tabular_data = self.aggregate(data, results)
|
||||||
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
|
def aggregate(self, data, results):
|
||||||
|
stake_currency = self.config.get('stake_currency')
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['pair', 'buy count', 'avg profit %',
|
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.currency == pair]
|
result = results[results.pair == pair]
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
result.profit_percent.mean() * 100.0,
|
||||||
result.profit_BTC.sum(),
|
result.profit_percent.sum() * 100.0,
|
||||||
result.duration.mean(),
|
result.profit_abs.sum(),
|
||||||
len(result[result.profit_BTC > 0]),
|
result.trade_duration.mean(),
|
||||||
len(result[result.profit_BTC < 0])
|
len(result[result.profit_abs > 0]),
|
||||||
|
len(result[result.profit_abs < 0])
|
||||||
])
|
])
|
||||||
|
|
||||||
# Append Total
|
# Append Total
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
'TOTAL',
|
'TOTAL',
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_percent.sum() * 100.0,
|
||||||
results.duration.mean(),
|
results.profit_abs.sum(),
|
||||||
len(results[results.profit_BTC > 0]),
|
results.trade_duration.mean(),
|
||||||
len(results[results.profit_BTC < 0])
|
len(results[results.profit_abs > 0]),
|
||||||
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
return floatfmt, headers, tabular_data
|
||||||
|
|
||||||
|
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
|
||||||
|
|
||||||
|
records = [(trade_entry.pair, trade_entry.profit_percent,
|
||||||
|
trade_entry.open_time.timestamp(),
|
||||||
|
trade_entry.close_time.timestamp(),
|
||||||
|
trade_entry.open_index - 1, trade_entry.trade_duration)
|
||||||
|
for index, trade_entry in results.iterrows()]
|
||||||
|
|
||||||
|
if records:
|
||||||
|
logger.info('Dumping backtest results to %s', recordfilename)
|
||||||
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
def _get_sell_trade_entry(
|
def _get_sell_trade_entry(
|
||||||
self, pair: str, buy_row: DataFrame,
|
self, pair: str, buy_row: DataFrame,
|
||||||
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
|
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
|
||||||
|
|
||||||
stake_amount = args['stake_amount']
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
@ -121,15 +154,32 @@ class Backtesting(object):
|
|||||||
buy_signal = sell_row.buy
|
buy_signal = sell_row.buy
|
||||||
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||||
sell_row.sell):
|
sell_row.sell):
|
||||||
return \
|
return BacktestResult(pair=pair,
|
||||||
sell_row, \
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
(
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
pair,
|
open_time=buy_row.date,
|
||||||
trade.calc_profit_percent(rate=sell_row.close),
|
close_time=sell_row.date,
|
||||||
trade.calc_profit(rate=sell_row.close),
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
(sell_row.date - buy_row.date).seconds // 60
|
open_index=buy_row.Index,
|
||||||
), \
|
close_index=sell_row.Index,
|
||||||
sell_row.date
|
open_at_end=False
|
||||||
|
)
|
||||||
|
if partial_ticker:
|
||||||
|
# no sell condition found - trade stil open at end of backtest period
|
||||||
|
sell_row = partial_ticker[-1]
|
||||||
|
btr = BacktestResult(pair=pair,
|
||||||
|
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
||||||
|
profit_abs=trade.calc_profit(rate=sell_row.close),
|
||||||
|
open_time=buy_row.date,
|
||||||
|
close_time=sell_row.date,
|
||||||
|
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
||||||
|
open_index=buy_row.Index,
|
||||||
|
close_index=sell_row.Index,
|
||||||
|
open_at_end=True
|
||||||
|
)
|
||||||
|
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||||
|
btr.profit_percent, btr.profit_abs)
|
||||||
|
return btr
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def backtest(self, args: Dict) -> DataFrame:
|
def backtest(self, args: Dict) -> DataFrame:
|
||||||
@ -145,17 +195,12 @@ class Backtesting(object):
|
|||||||
processed: a processed dictionary with format {pair, data}
|
processed: a processed dictionary with format {pair, data}
|
||||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
realistic: do we try to simulate realistic trades? (default: True)
|
realistic: do we try to simulate realistic trades? (default: True)
|
||||||
sell_profit_only: sell if profit only
|
|
||||||
use_sell_signal: act on sell-signal
|
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
realistic = args.get('realistic', False)
|
realistic = args.get('realistic', False)
|
||||||
record = args.get('record', None)
|
|
||||||
recordfilename = args.get('recordfn', 'backtest-result.json')
|
|
||||||
records = []
|
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: Dict = {}
|
trade_count_lock: Dict = {}
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
@ -170,6 +215,8 @@ class Backtesting(object):
|
|||||||
|
|
||||||
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
ticker_data.drop(ticker_data.head(1).index, inplace=True)
|
||||||
|
|
||||||
|
# Convert from Pandas to list for performance reasons
|
||||||
|
# (Looping Pandas is slow.)
|
||||||
ticker = [x for x in ticker_data.itertuples()]
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
lock_pair_until = None
|
lock_pair_until = None
|
||||||
@ -187,30 +234,20 @@ class Backtesting(object):
|
|||||||
|
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||||
|
|
||||||
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
|
||||||
trade_count_lock, args)
|
trade_count_lock, args)
|
||||||
|
|
||||||
if ret:
|
if trade_entry:
|
||||||
row2, trade_entry, next_date = ret
|
lock_pair_until = trade_entry.close_time
|
||||||
lock_pair_until = next_date
|
|
||||||
trades.append(trade_entry)
|
trades.append(trade_entry)
|
||||||
if record:
|
else:
|
||||||
# Note, need to be json.dump friendly
|
# Set lock_pair_until to end of testing period if trade could not be closed
|
||||||
# record a tuple of pair, current_profit_percent,
|
# This happens only if the buy-signal was with the last candle
|
||||||
# entry-date, duration
|
lock_pair_until = ticker_data.iloc[-1].date
|
||||||
records.append((pair, trade_entry[1],
|
|
||||||
row.date.strftime('%s'),
|
|
||||||
row2.date.strftime('%s'),
|
|
||||||
index, trade_entry[3]))
|
|
||||||
# For now export inside backtest(), maybe change so that backtest()
|
|
||||||
# returns a tuple like: (dataframe, records, logs, etc)
|
|
||||||
if record and record.find('trades') >= 0:
|
|
||||||
logger.info('Dumping backtest results to %s', recordfilename)
|
|
||||||
file_dump_json(recordfilename, records)
|
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
|
||||||
|
|
||||||
def start(self) -> None:
|
return DataFrame.from_records(trades, columns=BacktestResult._fields)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
"""
|
"""
|
||||||
Run a backtesting end-to-end
|
Run a backtesting end-to-end
|
||||||
:return: None
|
:return: None
|
||||||
@ -237,6 +274,9 @@ class Backtesting(object):
|
|||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
logger.critical("No data found. Terminating.")
|
||||||
|
return
|
||||||
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
||||||
if self.config.get('realistic_simulation', False):
|
if self.config.get('realistic_simulation', False):
|
||||||
max_open_trades = self.config['max_open_trades']
|
max_open_trades = self.config['max_open_trades']
|
||||||
@ -256,24 +296,22 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
|
|
||||||
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
|
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config.get('stake_amount'),
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
'processed': preprocessed,
|
'processed': preprocessed,
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'realistic': self.config.get('realistic_simulation', False),
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
'sell_profit_only': sell_profit_only,
|
|
||||||
'use_sell_signal': use_sell_signal,
|
|
||||||
'record': self.config.get('export'),
|
|
||||||
'recordfn': self.config.get('exportfilename'),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.config.get('export', False):
|
||||||
|
self._store_backtest_result(self.config.get('exportfilename'), results)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'\n==================================== '
|
'\n======================================== '
|
||||||
'BACKTESTING REPORT'
|
'BACKTESTING REPORT'
|
||||||
' ====================================\n'
|
' =========================================\n'
|
||||||
'%s',
|
'%s',
|
||||||
self._generate_text_table(
|
self._generate_text_table(
|
||||||
data,
|
data,
|
||||||
@ -281,6 +319,20 @@ class Backtesting(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'\n====================================== '
|
||||||
|
'LEFT OPEN TRADES REPORT'
|
||||||
|
' ======================================\n'
|
||||||
|
'%s',
|
||||||
|
self._generate_text_table(
|
||||||
|
data,
|
||||||
|
results.loc[results.open_at_end]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
table = self.aggregate(data, results)
|
||||||
|
return results, table
|
||||||
|
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
@ -19,7 +19,6 @@ from typing import Dict, Any, Callable, Optional
|
|||||||
import numpy
|
import numpy
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
||||||
from hyperopt.mongoexp import MongoTrials
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
@ -27,7 +26,6 @@ from freqtrade.arguments import Arguments
|
|||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.optimize import load_data
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.optimize.backtesting import Backtesting
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -222,9 +220,7 @@ class Hyperopt(Backtesting):
|
|||||||
results['result'],
|
results['result'],
|
||||||
results['loss']
|
results['loss']
|
||||||
)
|
)
|
||||||
print(log_msg)
|
|
||||||
else:
|
else:
|
||||||
print('.', end='')
|
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
|
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
|
||||||
@ -451,7 +447,7 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
total_profit = results.profit_percent.sum()
|
total_profit = results.profit_percent.sum()
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
trade_duration = results.duration.mean()
|
trade_duration = results.trade_duration.mean()
|
||||||
|
|
||||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
@ -488,10 +484,10 @@ class Hyperopt(Backtesting):
|
|||||||
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit_BTC.sum(),
|
results.profit_abs.sum(),
|
||||||
self.config['stake_currency'],
|
self.config['stake_currency'],
|
||||||
results.profit_percent.sum(),
|
results.profit_percent.sum(),
|
||||||
results.duration.mean(),
|
results.trade_duration.mean(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
@ -508,32 +504,20 @@ class Hyperopt(Backtesting):
|
|||||||
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
||||||
self.processed = self.tickerdata_to_dataframe(data)
|
self.processed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
if self.config.get('mongodb'):
|
logger.info('Preparing Trials..')
|
||||||
logger.info('Using mongodb ...')
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
# read trials file if we have one
|
||||||
|
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
||||||
|
self.trials = self.read_trials()
|
||||||
|
|
||||||
|
self.current_tries = len(self.trials.results)
|
||||||
|
self.total_tries += self.current_tries
|
||||||
logger.info(
|
logger.info(
|
||||||
'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!'
|
'Continuing with trials. Current: %d, Total: %d',
|
||||||
|
self.current_tries,
|
||||||
|
self.total_tries
|
||||||
)
|
)
|
||||||
|
|
||||||
db_name = 'freqtrade_hyperopt'
|
|
||||||
self.trials = MongoTrials(
|
|
||||||
arg='mongo://127.0.0.1:1234/{}/jobs'.format(db_name),
|
|
||||||
exp_key='exp1'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info('Preparing Trials..')
|
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
|
||||||
# read trials file if we have one
|
|
||||||
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
|
|
||||||
self.trials = self.read_trials()
|
|
||||||
|
|
||||||
self.current_tries = len(self.trials.results)
|
|
||||||
self.total_tries += self.current_tries
|
|
||||||
logger.info(
|
|
||||||
'Continuing with trials. Current: %d, Total: %d',
|
|
||||||
self.current_tries,
|
|
||||||
self.total_tries
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
best_parameters = fmin(
|
best_parameters = fmin(
|
||||||
fn=self.generate_optimizer,
|
fn=self.generate_optimizer,
|
||||||
@ -589,18 +573,14 @@ def start(args: Namespace) -> None:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Initialize configuration
|
# Initialize configuration
|
||||||
# Monkey patch the configuration with hyperopt_conf.py
|
# Monkey patch the configuration with hyperopt_conf.py
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
logger.info('Starting freqtrade in Hyperopt mode')
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
|
config = configuration.load_config()
|
||||||
|
|
||||||
optimize_config = hyperopt_optimize_conf()
|
|
||||||
config = configuration._load_common_config(optimize_config)
|
|
||||||
config = configuration._load_backtesting_config(config)
|
|
||||||
config = configuration._load_hyperopt_config(config)
|
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
@ -154,6 +154,12 @@ class Trade(_DECL_BASE):
|
|||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
close_date = Column(DateTime)
|
close_date = Column(DateTime)
|
||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
# absolute value of the stop loss
|
||||||
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the initial stop loss
|
||||||
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the highest reached price
|
||||||
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
||||||
@ -164,6 +170,50 @@ class Trade(_DECL_BASE):
|
|||||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def adjust_stop_loss(self, current_price, stoploss):
|
||||||
|
"""
|
||||||
|
|
||||||
|
this adjusts the stop loss to it's most recently observed
|
||||||
|
setting
|
||||||
|
:param current_price:
|
||||||
|
:param stoploss:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
new_loss = Decimal(current_price * (1 - abs(stoploss)))
|
||||||
|
|
||||||
|
# keeping track of the highest observed rate for this trade
|
||||||
|
if self.max_rate is None:
|
||||||
|
self.max_rate = current_price
|
||||||
|
else:
|
||||||
|
if current_price > self.max_rate:
|
||||||
|
self.max_rate = current_price
|
||||||
|
|
||||||
|
# no stop loss assigned yet
|
||||||
|
if self.stop_loss is None or self.stop_loss == 0:
|
||||||
|
logger.debug("assigning new stop loss")
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
self.initial_stop_loss = new_loss
|
||||||
|
|
||||||
|
# evaluate if the stop loss needs to be updated
|
||||||
|
else:
|
||||||
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
logger.debug("adjusted stop loss")
|
||||||
|
else:
|
||||||
|
logger.debug("keeping current stop loss")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"{} - current price {:.8f}, bought at {:.8f} and calculated "
|
||||||
|
"stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} "
|
||||||
|
"and max observed rate was {:.8f}".format(
|
||||||
|
self.pair, current_price, self.open_rate,
|
||||||
|
self.initial_stop_loss,
|
||||||
|
self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss),
|
||||||
|
self.max_rate
|
||||||
|
|
||||||
|
))
|
||||||
|
|
||||||
def update(self, order: Dict) -> None:
|
def update(self, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Updates this entity with amount and actual open/close rates.
|
Updates this entity with amount and actual open/close rates.
|
||||||
|
@ -2,24 +2,34 @@
|
|||||||
This module contains class to define a RPC communications
|
This module contains class to define a RPC communications
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from abc import abstractmethod
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Dict, Tuple, Any
|
from typing import Dict, Tuple, Any, List
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from pandas import DataFrame
|
|
||||||
from numpy import mean, nan_to_num
|
from numpy import mean, nan_to_num
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCException(Exception):
|
||||||
|
"""
|
||||||
|
Should be raised with a rpc-formatted message in an _rpc_* method
|
||||||
|
if the required state is wrong, i.e.:
|
||||||
|
|
||||||
|
raise RPCException('*Status:* `no active trade`')
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RPC(object):
|
class RPC(object):
|
||||||
"""
|
"""
|
||||||
RPC class can be used to have extra feature, like bot data, and access to DB data
|
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||||
@ -30,20 +40,32 @@ class RPC(object):
|
|||||||
:param freqtrade: Instance of a freqtrade bot
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.freqtrade = freqtrade
|
self._freqtrade = freqtrade
|
||||||
|
|
||||||
def rpc_trade_status(self) -> Tuple[bool, Any]:
|
@abstractmethod
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
""" Cleanup pending module resources """
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def name(self) -> str:
|
||||||
|
""" Returns the lowercase name of this module """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_msg(self, msg: str) -> None:
|
||||||
|
""" Sends a message to all registered rpc modules """
|
||||||
|
|
||||||
|
def _rpc_trade_status(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||||
a remotely exposed function
|
a remotely exposed function
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
# Fetch open trade
|
# Fetch open trade
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '*Status:* `trader is not running`'
|
raise RPCException('*Status:* `trader is not running`')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
return True, '*Status:* `no active trade`'
|
raise RPCException('*Status:* `no active trade`')
|
||||||
else:
|
else:
|
||||||
result = []
|
result = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -64,6 +86,7 @@ class RPC(object):
|
|||||||
"*Close Rate:* `{close_rate}`\n" \
|
"*Close Rate:* `{close_rate}`\n" \
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
"*Close Profit:* `{close_profit}`\n" \
|
"*Close Profit:* `{close_profit}`\n" \
|
||||||
|
"*Stake Value:* `{stake_value}`\n" \
|
||||||
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
||||||
"*Open Order:* `{open_order}`"\
|
"*Open Order:* `{open_order}`"\
|
||||||
.format(
|
.format(
|
||||||
@ -76,20 +99,21 @@ class RPC(object):
|
|||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
amount=round(trade.amount, 8),
|
amount=round(trade.amount, 8),
|
||||||
close_profit=fmt_close_profit,
|
close_profit=fmt_close_profit,
|
||||||
|
stake_value=round(current_rate * trade.amount, 8),
|
||||||
current_profit=round(current_profit * 100, 2),
|
current_profit=round(current_profit * 100, 2),
|
||||||
open_order='({} {} rem={:.8f})'.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
order['type'], order['side'], order['remaining']
|
order['type'], order['side'], order['remaining']
|
||||||
) if order else None,
|
) if order else None,
|
||||||
)
|
)
|
||||||
result.append(message)
|
result.append(message)
|
||||||
return False, result
|
return result
|
||||||
|
|
||||||
def rpc_status_table(self) -> Tuple[bool, Any]:
|
def _rpc_status_table(self) -> DataFrame:
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '*Status:* `trader is not running`'
|
raise RPCException('*Status:* `trader is not running`')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
return True, '*Status:* `no active order`'
|
raise RPCException('*Status:* `no active order`')
|
||||||
else:
|
else:
|
||||||
trades_list = []
|
trades_list = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -99,28 +123,25 @@ class RPC(object):
|
|||||||
trade.id,
|
trade.id,
|
||||||
trade.pair,
|
trade.pair,
|
||||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)),
|
||||||
|
'{:.8f}'.format(trade.amount * current_rate)
|
||||||
])
|
])
|
||||||
|
|
||||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
columns = ['ID', 'Pair', 'Since', 'Profit', 'Value']
|
||||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||||
df_statuses = df_statuses.set_index(columns[0])
|
df_statuses = df_statuses.set_index(columns[0])
|
||||||
# The style used throughout is to return a tuple
|
return df_statuses
|
||||||
# consisting of (error_occured?, result)
|
|
||||||
# Another approach would be to just return the
|
|
||||||
# result, or raise error
|
|
||||||
return False, df_statuses
|
|
||||||
|
|
||||||
def rpc_daily_profit(
|
def _rpc_daily_profit(
|
||||||
self, timescale: int,
|
self, timescale: int,
|
||||||
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
|
||||||
today = datetime.utcnow().date()
|
today = datetime.utcnow().date()
|
||||||
profit_days: Dict[date, Dict] = {}
|
profit_days: Dict[date, Dict] = {}
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
|
||||||
|
|
||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self._freqtrade.fiat_converter
|
||||||
for day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = today - timedelta(days=day)
|
||||||
trades = Trade.query \
|
trades = Trade.query \
|
||||||
@ -135,7 +156,7 @@ class RPC(object):
|
|||||||
'trades': len(trades)
|
'trades': len(trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats = [
|
return [
|
||||||
[
|
[
|
||||||
key,
|
key,
|
||||||
'{value:.8f} {symbol}'.format(
|
'{value:.8f} {symbol}'.format(
|
||||||
@ -157,13 +178,10 @@ class RPC(object):
|
|||||||
]
|
]
|
||||||
for key, value in profit_days.items()
|
for key, value in profit_days.items()
|
||||||
]
|
]
|
||||||
return False, stats
|
|
||||||
|
|
||||||
def rpc_trade_statistics(
|
def _rpc_trade_statistics(
|
||||||
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
"""
|
""" Returns cumulative profit statistics """
|
||||||
:return: cumulative profit statistics.
|
|
||||||
"""
|
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_all_coin = []
|
profit_all_coin = []
|
||||||
@ -201,13 +219,13 @@ class RPC(object):
|
|||||||
.order_by(sql.text('profit_sum DESC')).first()
|
.order_by(sql.text('profit_sum DESC')).first()
|
||||||
|
|
||||||
if not best_pair:
|
if not best_pair:
|
||||||
return True, '*Status:* `no closed trade`'
|
raise RPCException('*Status:* `no closed trade`')
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
# FIX: we want to keep fiatconverter in a state/environment,
|
# FIX: we want to keep fiatconverter in a state/environment,
|
||||||
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self._freqtrade.fiat_converter
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||||
@ -224,35 +242,29 @@ class RPC(object):
|
|||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
)
|
||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
return (
|
return {
|
||||||
False,
|
'profit_closed_coin': profit_closed_coin,
|
||||||
{
|
'profit_closed_percent': profit_closed_percent,
|
||||||
'profit_closed_coin': profit_closed_coin,
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
'profit_closed_percent': profit_closed_percent,
|
'profit_all_coin': profit_all_coin,
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
'profit_all_percent': profit_all_percent,
|
||||||
'profit_all_coin': profit_all_coin,
|
'profit_all_fiat': profit_all_fiat,
|
||||||
'profit_all_percent': profit_all_percent,
|
'trade_count': len(trades),
|
||||||
'profit_all_fiat': profit_all_fiat,
|
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
||||||
'trade_count': len(trades),
|
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
||||||
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
'best_pair': bp_pair,
|
||||||
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
'best_rate': round(bp_rate * 100, 2),
|
||||||
'best_pair': bp_pair,
|
}
|
||||||
'best_rate': round(bp_rate * 100, 2)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
|
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
|
||||||
"""
|
""" Returns current account balance per crypto """
|
||||||
:return: current account balance per crypto
|
|
||||||
"""
|
|
||||||
output = []
|
output = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
for coin, balance in exchange.get_balances().items():
|
for coin, balance in exchange.get_balances().items():
|
||||||
if not balance['total']:
|
if not balance['total']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rate = None
|
|
||||||
if coin == 'BTC':
|
if coin == 'BTC':
|
||||||
rate = 1.0
|
rate = 1.0
|
||||||
else:
|
else:
|
||||||
@ -272,39 +284,39 @@ class RPC(object):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if total == 0.0:
|
if total == 0.0:
|
||||||
return True, '`All balances are zero.`'
|
raise RPCException('`All balances are zero.`')
|
||||||
|
|
||||||
fiat = self.freqtrade.fiat_converter
|
fiat = self._freqtrade.fiat_converter
|
||||||
symbol = fiat_display_currency
|
symbol = fiat_display_currency
|
||||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||||
return False, (output, total, symbol, value)
|
return output, total, symbol, value
|
||||||
|
|
||||||
def rpc_start(self) -> Tuple[bool, str]:
|
def _rpc_start(self) -> str:
|
||||||
"""
|
""" Handler for start """
|
||||||
Handler for start.
|
if self._freqtrade.state == State.RUNNING:
|
||||||
"""
|
return '*Status:* `already running`'
|
||||||
if self.freqtrade.state == State.RUNNING:
|
|
||||||
return True, '*Status:* `already running`'
|
|
||||||
|
|
||||||
self.freqtrade.state = State.RUNNING
|
self._freqtrade.state = State.RUNNING
|
||||||
return False, '`Starting trader ...`'
|
return '`Starting trader ...`'
|
||||||
|
|
||||||
def rpc_stop(self) -> Tuple[bool, str]:
|
def _rpc_stop(self) -> str:
|
||||||
"""
|
""" Handler for stop """
|
||||||
Handler for stop.
|
if self._freqtrade.state == State.RUNNING:
|
||||||
"""
|
self._freqtrade.state = State.STOPPED
|
||||||
if self.freqtrade.state == State.RUNNING:
|
return '`Stopping trader ...`'
|
||||||
self.freqtrade.state = State.STOPPED
|
|
||||||
return False, '`Stopping trader ...`'
|
|
||||||
|
|
||||||
return True, '*Status:* `already stopped`'
|
return '*Status:* `already stopped`'
|
||||||
|
|
||||||
|
def _rpc_reload_conf(self) -> str:
|
||||||
|
""" Handler for reload_conf. """
|
||||||
|
self._freqtrade.state = State.RELOAD_CONF
|
||||||
|
return '*Status:* `Reloading config ...`'
|
||||||
|
|
||||||
# FIX: no test for this!!!!
|
# FIX: no test for this!!!!
|
||||||
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
|
def _rpc_forcesell(self, trade_id) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
:return: error or None
|
|
||||||
"""
|
"""
|
||||||
def _exec_forcesell(trade: Trade) -> None:
|
def _exec_forcesell(trade: Trade) -> None:
|
||||||
# Check if there is there is an open order
|
# Check if there is there is an open order
|
||||||
@ -330,17 +342,17 @@ class RPC(object):
|
|||||||
|
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
self.freqtrade.execute_sell(trade, current_rate)
|
self._freqtrade.execute_sell(trade, current_rate)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '`trader is not running`'
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
return False, ''
|
return
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
trade = Trade.query.filter(
|
trade = Trade.query.filter(
|
||||||
@ -351,19 +363,18 @@ class RPC(object):
|
|||||||
).first()
|
).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
logger.warning('forcesell: Invalid argument received')
|
logger.warning('forcesell: Invalid argument received')
|
||||||
return True, 'Invalid argument.'
|
raise RPCException('Invalid argument.')
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
return False, ''
|
|
||||||
|
|
||||||
def rpc_performance(self) -> Tuple[bool, Any]:
|
def _rpc_performance(self) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Handler for performance.
|
Handler for performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
if self.freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
return True, '`trader is not running`'
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
pair_rates = Trade.session.query(Trade.pair,
|
pair_rates = Trade.session.query(Trade.pair,
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
@ -372,19 +383,14 @@ class RPC(object):
|
|||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(sql.text('profit_sum DESC')) \
|
.order_by(sql.text('profit_sum DESC')) \
|
||||||
.all()
|
.all()
|
||||||
trades = []
|
return [
|
||||||
for (pair, rate, count) in pair_rates:
|
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
|
||||||
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
|
for pair, rate, count in pair_rates
|
||||||
|
]
|
||||||
|
|
||||||
return False, trades
|
def _rpc_count(self) -> List[Trade]:
|
||||||
|
""" Returns the number of trades running """
|
||||||
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
raise RPCException('`trader is not running`')
|
||||||
|
|
||||||
def rpc_count(self) -> Tuple[bool, Any]:
|
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
"""
|
|
||||||
Returns the number of trades running
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.freqtrade.state != State.RUNNING:
|
|
||||||
return True, '`trader is not running`'
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
return False, trades
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
from typing import Any, List
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.rpc import RPC
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -15,36 +14,23 @@ class RPCManager(object):
|
|||||||
Class to manage RPC objects (Telegram, Slack, ...)
|
Class to manage RPC objects (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
""" Initializes all enabled rpc modules """
|
||||||
Initializes all enabled rpc modules
|
self.registered_modules: List[RPC] = []
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.freqtrade = freqtrade
|
|
||||||
|
|
||||||
self.registered_modules: List[str] = []
|
# Enable telegram
|
||||||
self.telegram: Any = None
|
if freqtrade.config['telegram'].get('enabled', False):
|
||||||
self._init()
|
|
||||||
|
|
||||||
def _init(self) -> None:
|
|
||||||
"""
|
|
||||||
Init RPC modules
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if self.freqtrade.config['telegram'].get('enabled', False):
|
|
||||||
logger.info('Enabling rpc.telegram ...')
|
logger.info('Enabling rpc.telegram ...')
|
||||||
self.registered_modules.append('telegram')
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.telegram = Telegram(self.freqtrade)
|
self.registered_modules.append(Telegram(freqtrade))
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
"""
|
""" Stops all enabled rpc modules """
|
||||||
Stops all enabled rpc modules
|
logger.info('Cleaning up rpc modules ...')
|
||||||
:return: None
|
while self.registered_modules:
|
||||||
"""
|
mod = self.registered_modules.pop()
|
||||||
if 'telegram' in self.registered_modules:
|
logger.debug('Cleaning up rpc.%s ...', mod.name)
|
||||||
logger.info('Cleaning up rpc.telegram ...')
|
mod.cleanup()
|
||||||
self.registered_modules.remove('telegram')
|
del mod
|
||||||
self.telegram.cleanup()
|
|
||||||
|
|
||||||
def send_msg(self, msg: str) -> None:
|
def send_msg(self, msg: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -52,6 +38,7 @@ class RPCManager(object):
|
|||||||
:param msg: message
|
:param msg: message
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
logger.info(msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
if 'telegram' in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
self.telegram.send_msg(msg)
|
logger.debug('Forwarding message to rpc.%s', mod.name)
|
||||||
|
mod.send_msg(msg)
|
||||||
|
@ -12,11 +12,12 @@ from telegram.error import NetworkError, TelegramError
|
|||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.debug('Included module rpc.telegram ...')
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
@ -25,9 +26,7 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
|
|||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
"""
|
""" Decorator logic """
|
||||||
Decorator logic
|
|
||||||
"""
|
|
||||||
update = kwargs.get('update') or args[1]
|
update = kwargs.get('update') or args[1]
|
||||||
|
|
||||||
# Reject unauthorized messages
|
# Reject unauthorized messages
|
||||||
@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
|
|||||||
|
|
||||||
|
|
||||||
class Telegram(RPC):
|
class Telegram(RPC):
|
||||||
"""
|
""" This class handles all telegram communication """
|
||||||
Telegram, this class send messages to Telegram
|
|
||||||
"""
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "telegram"
|
||||||
|
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
"""
|
||||||
Init the Telegram call, and init the super class RPC
|
Init the Telegram call, and init the super class RPC
|
||||||
@ -74,12 +76,7 @@ class Telegram(RPC):
|
|||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
registers all known command handlers
|
registers all known command handlers
|
||||||
and starts polling for message updates
|
and starts polling for message updates
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||||
|
|
||||||
# Register command handler and start telegram message polling
|
# Register command handler and start telegram message polling
|
||||||
@ -93,6 +90,7 @@ class Telegram(RPC):
|
|||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
|
CommandHandler('reload_conf', self._reload_conf),
|
||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
CommandHandler('version', self._version),
|
||||||
]
|
]
|
||||||
@ -114,16 +112,11 @@ class Telegram(RPC):
|
|||||||
Stops all running telegram threads.
|
Stops all running telegram threads.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
self._updater.stop()
|
self._updater.stop()
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def send_msg(self, msg: str) -> None:
|
||||||
"""
|
""" Send a message to telegram channel """
|
||||||
Returns True if the telegram module is activated, False otherwise
|
self._send_msg(msg)
|
||||||
"""
|
|
||||||
return bool(self._config.get('telegram', {}).get('enabled', False))
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(self, bot: Bot, update: Update) -> None:
|
def _status(self, bot: Bot, update: Update) -> None:
|
||||||
@ -142,13 +135,11 @@ class Telegram(RPC):
|
|||||||
self._status_table(bot, update)
|
self._status_table(bot, update)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fetch open trade
|
try:
|
||||||
(error, trades) = self.rpc_trade_status()
|
for trade_msg in self._rpc_trade_status():
|
||||||
if error:
|
self._send_msg(trade_msg, bot=bot)
|
||||||
self.send_msg(trades, bot=bot)
|
except RPCException as e:
|
||||||
else:
|
self._send_msg(str(e), bot=bot)
|
||||||
for trademsg in trades:
|
|
||||||
self.send_msg(trademsg, bot=bot)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status_table(self, bot: Bot, update: Update) -> None:
|
def _status_table(self, bot: Bot, update: Update) -> None:
|
||||||
@ -159,15 +150,12 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Fetch open trade
|
try:
|
||||||
(err, df_statuses) = self.rpc_status_table()
|
df_statuses = self._rpc_status_table()
|
||||||
if err:
|
|
||||||
self.send_msg(df_statuses, bot=bot)
|
|
||||||
else:
|
|
||||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||||
message = "<pre>{}</pre>".format(message)
|
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
|
||||||
|
except RPCException as e:
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _daily(self, bot: Bot, update: Update) -> None:
|
def _daily(self, bot: Bot, update: Update) -> None:
|
||||||
@ -182,27 +170,25 @@ class Telegram(RPC):
|
|||||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
timescale = 7
|
timescale = 7
|
||||||
(error, stats) = self.rpc_daily_profit(
|
try:
|
||||||
timescale,
|
stats = self._rpc_daily_profit(
|
||||||
self._config['stake_currency'],
|
timescale,
|
||||||
self._config['fiat_display_currency']
|
self._config['stake_currency'],
|
||||||
)
|
self._config['fiat_display_currency']
|
||||||
if error:
|
)
|
||||||
self.send_msg(stats, bot=bot)
|
|
||||||
else:
|
|
||||||
stats = tabulate(stats,
|
stats = tabulate(stats,
|
||||||
headers=[
|
headers=[
|
||||||
'Day',
|
'Day',
|
||||||
'Profit {}'.format(self._config['stake_currency']),
|
'Profit {}'.format(self._config['stake_currency']),
|
||||||
'Profit {}'.format(self._config['fiat_display_currency'])
|
'Profit {}'.format(self._config['fiat_display_currency']),
|
||||||
|
'Trades'
|
||||||
],
|
],
|
||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
||||||
.format(
|
.format(timescale, stats)
|
||||||
timescale,
|
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
stats
|
except RPCException as e:
|
||||||
)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(self, bot: Bot, update: Update) -> None:
|
def _profit(self, bot: Bot, update: Update) -> None:
|
||||||
@ -213,67 +199,63 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, stats) = self.rpc_trade_statistics(
|
try:
|
||||||
self._config['stake_currency'],
|
stats = self._rpc_trade_statistics(
|
||||||
self._config['fiat_display_currency']
|
self._config['stake_currency'],
|
||||||
)
|
self._config['fiat_display_currency'])
|
||||||
if error:
|
|
||||||
self.send_msg(stats, bot=bot)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Message to display
|
# Message to display
|
||||||
markdown_msg = "*ROI:* Close trades\n" \
|
markdown_msg = "*ROI:* Close trades\n" \
|
||||||
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
||||||
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
||||||
"*ROI:* All trades\n" \
|
"*ROI:* All trades\n" \
|
||||||
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
||||||
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
||||||
"*Total Trade Count:* `{trade_count}`\n" \
|
"*Total Trade Count:* `{trade_count}`\n" \
|
||||||
"*First Trade opened:* `{first_trade_date}`\n" \
|
"*First Trade opened:* `{first_trade_date}`\n" \
|
||||||
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||||
"*Avg. Duration:* `{avg_duration}`\n" \
|
"*Avg. Duration:* `{avg_duration}`\n" \
|
||||||
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
||||||
.format(
|
.format(
|
||||||
coin=self._config['stake_currency'],
|
coin=self._config['stake_currency'],
|
||||||
fiat=self._config['fiat_display_currency'],
|
fiat=self._config['fiat_display_currency'],
|
||||||
profit_closed_coin=stats['profit_closed_coin'],
|
profit_closed_coin=stats['profit_closed_coin'],
|
||||||
profit_closed_percent=stats['profit_closed_percent'],
|
profit_closed_percent=stats['profit_closed_percent'],
|
||||||
profit_closed_fiat=stats['profit_closed_fiat'],
|
profit_closed_fiat=stats['profit_closed_fiat'],
|
||||||
profit_all_coin=stats['profit_all_coin'],
|
profit_all_coin=stats['profit_all_coin'],
|
||||||
profit_all_percent=stats['profit_all_percent'],
|
profit_all_percent=stats['profit_all_percent'],
|
||||||
profit_all_fiat=stats['profit_all_fiat'],
|
profit_all_fiat=stats['profit_all_fiat'],
|
||||||
trade_count=stats['trade_count'],
|
trade_count=stats['trade_count'],
|
||||||
first_trade_date=stats['first_trade_date'],
|
first_trade_date=stats['first_trade_date'],
|
||||||
latest_trade_date=stats['latest_trade_date'],
|
latest_trade_date=stats['latest_trade_date'],
|
||||||
avg_duration=stats['avg_duration'],
|
avg_duration=stats['avg_duration'],
|
||||||
best_pair=stats['best_pair'],
|
best_pair=stats['best_pair'],
|
||||||
best_rate=stats['best_rate']
|
best_rate=stats['best_rate']
|
||||||
)
|
)
|
||||||
self.send_msg(markdown_msg, bot=bot)
|
self._send_msg(markdown_msg, bot=bot)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _balance(self, bot: Bot, update: Update) -> None:
|
def _balance(self, bot: Bot, update: Update) -> None:
|
||||||
"""
|
""" Handler for /balance """
|
||||||
Handler for /balance
|
try:
|
||||||
"""
|
currencys, total, symbol, value = \
|
||||||
(error, result) = self.rpc_balance(self._config['fiat_display_currency'])
|
self._rpc_balance(self._config['fiat_display_currency'])
|
||||||
if error:
|
output = ''
|
||||||
self.send_msg('`All balances are zero.`')
|
for currency in currencys:
|
||||||
return
|
output += "*{currency}:*\n" \
|
||||||
|
"\t`Available: {available: .8f}`\n" \
|
||||||
|
"\t`Balance: {balance: .8f}`\n" \
|
||||||
|
"\t`Pending: {pending: .8f}`\n" \
|
||||||
|
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||||
|
|
||||||
(currencys, total, symbol, value) = result
|
output += "\n*Estimated Value*:\n" \
|
||||||
output = ''
|
"\t`BTC: {0: .8f}`\n" \
|
||||||
for currency in currencys:
|
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
|
||||||
output += "*{currency}:*\n" \
|
self._send_msg(output, bot=bot)
|
||||||
"\t`Available: {available: .8f}`\n" \
|
except RPCException as e:
|
||||||
"\t`Balance: {balance: .8f}`\n" \
|
self._send_msg(str(e), bot=bot)
|
||||||
"\t`Pending: {pending: .8f}`\n" \
|
|
||||||
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
|
||||||
|
|
||||||
output += "\n*Estimated Value*:\n" \
|
|
||||||
"\t`BTC: {0: .8f}`\n" \
|
|
||||||
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
|
|
||||||
self.send_msg(output)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _start(self, bot: Bot, update: Update) -> None:
|
def _start(self, bot: Bot, update: Update) -> None:
|
||||||
@ -284,9 +266,8 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, msg) = self.rpc_start()
|
msg = self._rpc_start()
|
||||||
if error:
|
self._send_msg(msg, bot=bot)
|
||||||
self.send_msg(msg, bot=bot)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stop(self, bot: Bot, update: Update) -> None:
|
def _stop(self, bot: Bot, update: Update) -> None:
|
||||||
@ -297,8 +278,20 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, msg) = self.rpc_stop()
|
msg = self._rpc_stop()
|
||||||
self.send_msg(msg, bot=bot)
|
self._send_msg(msg, bot=bot)
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /reload_conf.
|
||||||
|
Triggers a config file reload
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
msg = self._rpc_reload_conf()
|
||||||
|
self._send_msg(msg, bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||||
@ -311,10 +304,10 @@ class Telegram(RPC):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
trade_id = update.message.text.replace('/forcesell', '').strip()
|
trade_id = update.message.text.replace('/forcesell', '').strip()
|
||||||
(error, message) = self.rpc_forcesell(trade_id)
|
try:
|
||||||
if error:
|
self._rpc_forcesell(trade_id)
|
||||||
self.send_msg(message, bot=bot)
|
except RPCException as e:
|
||||||
return
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(self, bot: Bot, update: Update) -> None:
|
def _performance(self, bot: Bot, update: Update) -> None:
|
||||||
@ -325,19 +318,18 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, trades) = self.rpc_performance()
|
try:
|
||||||
if error:
|
trades = self._rpc_performance()
|
||||||
self.send_msg(trades, bot=bot)
|
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
||||||
return
|
index=i + 1,
|
||||||
|
pair=trade['pair'],
|
||||||
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
profit=trade['profit'],
|
||||||
index=i + 1,
|
count=trade['count']
|
||||||
pair=trade['pair'],
|
) for i, trade in enumerate(trades))
|
||||||
profit=trade['profit'],
|
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||||
count=trade['count']
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
) for i, trade in enumerate(trades))
|
except RPCException as e:
|
||||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _count(self, bot: Bot, update: Update) -> None:
|
def _count(self, bot: Bot, update: Update) -> None:
|
||||||
@ -348,19 +340,18 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
(error, trades) = self.rpc_count()
|
try:
|
||||||
if error:
|
trades = self._rpc_count()
|
||||||
self.send_msg(trades, bot=bot)
|
message = tabulate({
|
||||||
return
|
'current': [len(trades)],
|
||||||
|
'max': [self._config['max_open_trades']],
|
||||||
message = tabulate({
|
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
|
||||||
'current': [len(trades)],
|
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
|
||||||
'max': [self._config['max_open_trades']],
|
message = "<pre>{}</pre>".format(message)
|
||||||
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
|
logger.debug(message)
|
||||||
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
message = "<pre>{}</pre>".format(message)
|
except RPCException as e:
|
||||||
logger.debug(message)
|
self._send_msg(str(e), bot=bot)
|
||||||
self.send_msg(message, parse_mode=ParseMode.HTML)
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _help(self, bot: Bot, update: Update) -> None:
|
def _help(self, bot: Bot, update: Update) -> None:
|
||||||
@ -386,7 +377,7 @@ class Telegram(RPC):
|
|||||||
"*/help:* `This help message`\n" \
|
"*/help:* `This help message`\n" \
|
||||||
"*/version:* `Show version`"
|
"*/version:* `Show version`"
|
||||||
|
|
||||||
self.send_msg(message, bot=bot)
|
self._send_msg(message, bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _version(self, bot: Bot, update: Update) -> None:
|
def _version(self, bot: Bot, update: Update) -> None:
|
||||||
@ -397,10 +388,10 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||||
|
|
||||||
def send_msg(self, msg: str, bot: Bot = None,
|
def _send_msg(self, msg: str, bot: Bot = None,
|
||||||
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
:param msg: message
|
:param msg: message
|
||||||
@ -408,9 +399,6 @@ class Telegram(RPC):
|
|||||||
:param parse_mode: telegram parse mode
|
:param parse_mode: telegram parse mode
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if not self.is_enabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
bot = bot or self._updater.bot
|
bot = bot or self._updater.bot
|
||||||
|
|
||||||
keyboard = [['/daily', '/profit', '/balance'],
|
keyboard = [['/daily', '/profit', '/balance'],
|
||||||
|
@ -8,7 +8,8 @@ import enum
|
|||||||
|
|
||||||
class State(enum.Enum):
|
class State(enum.Enum):
|
||||||
"""
|
"""
|
||||||
Bot running states
|
Bot application states
|
||||||
"""
|
"""
|
||||||
RUNNING = 0
|
RUNNING = 0
|
||||||
STOPPED = 1
|
STOPPED = 1
|
||||||
|
RELOAD_CONF = 2
|
||||||
|
@ -16,10 +16,10 @@ class DefaultStrategy(IStrategy):
|
|||||||
|
|
||||||
# Minimal ROI designed for the strategy
|
# Minimal ROI designed for the strategy
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
"20": 0.02,
|
"20": 0.02,
|
||||||
"0": 0.04
|
"0": 0.04
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
# Optimal stoploss designed for the strategy
|
||||||
@ -204,14 +204,14 @@ class DefaultStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['rsi'] < 35) &
|
(dataframe['rsi'] < 35) &
|
||||||
(dataframe['fastd'] < 35) &
|
(dataframe['fastd'] < 35) &
|
||||||
(dataframe['adx'] > 30) &
|
(dataframe['adx'] > 30) &
|
||||||
(dataframe['plus_di'] > 0.5)
|
(dataframe['plus_di'] > 0.5)
|
||||||
) |
|
) |
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 65) &
|
(dataframe['adx'] > 65) &
|
||||||
(dataframe['plus_di'] > 0.5)
|
(dataframe['plus_di'] > 0.5)
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
|
|
||||||
@ -225,16 +225,16 @@ class DefaultStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
||||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||||
) &
|
) &
|
||||||
(dataframe['adx'] > 10) &
|
(dataframe['adx'] > 10) &
|
||||||
(dataframe['minus_di'] > 0)
|
(dataframe['minus_di'] > 0)
|
||||||
) |
|
) |
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 70) &
|
(dataframe['adx'] > 70) &
|
||||||
(dataframe['minus_di'] > 0.5)
|
(dataframe['minus_di'] > 0.5)
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
IStrategy interface
|
IStrategy interface
|
||||||
This module defines the interface to apply for strategies
|
This module defines the interface to apply for strategies
|
||||||
"""
|
"""
|
||||||
|
import warnings
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
from abc import ABC
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
|
||||||
class IStrategy(ABC):
|
class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
Interface for freqtrade strategies
|
Interface for freqtrade strategies
|
||||||
@ -19,30 +19,71 @@ class IStrategy(ABC):
|
|||||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# associated minimal roi
|
||||||
minimal_roi: Dict
|
minimal_roi: Dict
|
||||||
|
|
||||||
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
|
||||||
|
# associated ticker interval
|
||||||
ticker_interval: str
|
ticker_interval: str
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Populate indicators that will be used in the Buy and Sell strategy
|
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()
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
warnings.warn("deprecated - please replace this method with advise_indicators!", DeprecationWarning)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy signal for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
|
warnings.warn("deprecated - please replace this method with advise_buy!", DeprecationWarning)
|
||||||
|
dataframe.loc[(), 'buy'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the sell signal for the given dataframe
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
|
warnings.warn("deprecated - please replace this method with advise_sell!", DeprecationWarning)
|
||||||
|
dataframe.loc[(), 'sell'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def advise_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
|
||||||
|
This wraps around the internal method
|
||||||
|
|
||||||
|
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()
|
||||||
|
:param pair: The currently traded pair
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
|
"""
|
||||||
|
return self.populate_indicators(dataframe)
|
||||||
|
|
||||||
|
def advise_buy(self, dataframe: DataFrame, pair: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:param pair: The currently traded pair
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.populate_buy_trend(dataframe)
|
||||||
|
|
||||||
|
def advise_sell(self, dataframe: DataFrame, pair: str) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:param pair: The currently traded pair
|
||||||
|
:return: DataFrame with sell column
|
||||||
|
"""
|
||||||
|
return self.populate_sell_trend(dataframe)
|
||||||
|
@ -6,13 +6,17 @@ This module load custom strategies
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
from base64 import urlsafe_b64decode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Optional, Dict, Type
|
from typing import Optional, Dict, Type
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
import tempfile
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -61,6 +65,13 @@ class StrategyResolver(object):
|
|||||||
key=lambda t: t[0]))
|
key=lambda t: t[0]))
|
||||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
|
|
||||||
|
def compile(self, strategy_name: str, strategy_content: str) -> Optional[IStrategy]:
|
||||||
|
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
||||||
|
temp.joinpath(strategy_name + ".py").write_text(strategy_content)
|
||||||
|
temp.joinpath("__init__.py").touch()
|
||||||
|
|
||||||
|
return self._load_strategy(strategy_name, temp.absolute())
|
||||||
|
|
||||||
def _load_strategy(
|
def _load_strategy(
|
||||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
@ -79,6 +90,48 @@ class StrategyResolver(object):
|
|||||||
# 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, extra_dir)
|
||||||
|
|
||||||
|
# check if the given strategy is provided as name, value pair
|
||||||
|
# where the value is the strategy encoded in base 64
|
||||||
|
if ":" in strategy_name and "http" not in strategy_name:
|
||||||
|
strat = strategy_name.split(":")
|
||||||
|
|
||||||
|
if len(strat) == 2:
|
||||||
|
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
||||||
|
name = strat[0] + ".py"
|
||||||
|
|
||||||
|
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
|
||||||
|
temp.joinpath("__init__.py").touch()
|
||||||
|
|
||||||
|
strategy_name = os.path.splitext(name)[0]
|
||||||
|
|
||||||
|
# register temp path with the bot
|
||||||
|
abs_paths.insert(0, temp.absolute())
|
||||||
|
|
||||||
|
# check if given strategy matches an url
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
logger.debug("requesting remote strategy from {}".format(strategy_name))
|
||||||
|
resp = requests.get(strategy_name, stream=True)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
||||||
|
|
||||||
|
if strategy_name.endswith("/code"):
|
||||||
|
strategy_name = strategy_name.replace("/code", "")
|
||||||
|
|
||||||
|
name = os.path.basename(urlparse(strategy_name).path)
|
||||||
|
|
||||||
|
temp.joinpath("{}.py".format(name)).write_text(resp.text)
|
||||||
|
temp.joinpath("__init__.py").touch()
|
||||||
|
|
||||||
|
strategy_name = os.path.splitext(name)[0]
|
||||||
|
|
||||||
|
# print("stored downloaded stat at: {}".format(temp))
|
||||||
|
# register temp path with the bot
|
||||||
|
abs_paths.insert(0, temp.absolute())
|
||||||
|
|
||||||
|
except requests.RequestException:
|
||||||
|
logger.debug("received error trying to fetch strategy remotely, carry on!")
|
||||||
|
|
||||||
for path in abs_paths:
|
for path in abs_paths:
|
||||||
strategy = self._search_strategy(path, strategy_name)
|
strategy = self._search_strategy(path, strategy_name)
|
||||||
if strategy:
|
if strategy:
|
||||||
|
@ -15,6 +15,10 @@ from freqtrade.analyze import Analyze
|
|||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
|
import moto
|
||||||
|
import boto3
|
||||||
|
import os
|
||||||
|
|
||||||
logging.getLogger('').setLevel(logging.INFO)
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
@ -85,9 +89,21 @@ def default_conf():
|
|||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"disable_buy": False,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30
|
||||||
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"use_book_order": False,
|
||||||
|
"book_order_top": 6,
|
||||||
|
"ask_last_balance": 0.0,
|
||||||
|
"percent_from_top": 0.0
|
||||||
|
},
|
||||||
|
"ask_strategy": {
|
||||||
|
"use_book_order": False,
|
||||||
|
"book_order_min": 1,
|
||||||
|
"book_order_max": 10
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -241,7 +257,8 @@ def limit_buy_order():
|
|||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'status': 'closed'
|
'status': 'closed',
|
||||||
|
'filled': 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -256,7 +273,8 @@ def limit_buy_order_old():
|
|||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
'status': 'open'
|
'status': 'open',
|
||||||
|
'filled': 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -271,7 +289,8 @@ def limit_sell_order_old():
|
|||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 90.99181073,
|
'remaining': 90.99181073,
|
||||||
'status': 'open'
|
'status': 'open',
|
||||||
|
'filled': 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -286,7 +305,8 @@ def limit_buy_order_old_partial():
|
|||||||
'price': 0.00001099,
|
'price': 0.00001099,
|
||||||
'amount': 90.99181073,
|
'amount': 90.99181073,
|
||||||
'remaining': 67.99181073,
|
'remaining': 67.99181073,
|
||||||
'status': 'open'
|
'status': 'open',
|
||||||
|
'filled': 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -516,6 +536,7 @@ def result():
|
|||||||
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
||||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
# Create an fixture/function
|
# Create an fixture/function
|
||||||
# that inserts a trade of some type and open-status
|
# that inserts a trade of some type and open-status
|
||||||
|
@ -520,7 +520,6 @@ def test_get_order(default_conf, mocker):
|
|||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||||
print(exchange.get_order('X', 'TKN/BTC'))
|
|
||||||
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
||||||
|
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
@ -9,6 +9,7 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import optimize
|
from freqtrade import optimize
|
||||||
@ -84,6 +85,7 @@ def load_data_test(what):
|
|||||||
|
|
||||||
def simple_backtest(config, contour, num_results, mocker) -> None:
|
def simple_backtest(config, contour, num_results, mocker) -> None:
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
|
|
||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
@ -97,6 +99,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
|||||||
'realistic': True
|
'realistic': True
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
assert len(results) == num_results
|
assert len(results) == num_results
|
||||||
|
|
||||||
@ -353,28 +356,35 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
|
|
||||||
results = pd.DataFrame(
|
results = pd.DataFrame(
|
||||||
{
|
{
|
||||||
'currency': ['ETH/BTC', 'ETH/BTC'],
|
'pair': ['ETH/BTC', 'ETH/BTC'],
|
||||||
'profit_percent': [0.1, 0.2],
|
'profit_percent': [0.1, 0.2],
|
||||||
'profit_BTC': [0.2, 0.4],
|
'profit_abs': [0.2, 0.4],
|
||||||
'duration': [10, 30],
|
'cum profit %': [30, 30],
|
||||||
|
'total profit BTC': [0.6, 0.6],
|
||||||
|
'trade_duration': [10, 30],
|
||||||
'profit': [2, 0],
|
'profit': [2, 0],
|
||||||
'loss': [0, 0]
|
'loss': [0, 0]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| pair | buy count | avg profit % | '
|
"""| pair | buy count | avg profit % | cum profit % | total profit BTC | avg duration | profit | loss |
|
||||||
'total profit BTC | avg duration | profit | loss |\n'
|
|:--------|------------:|---------------:|---------------:|-------------------:|---------------:|---------:|-------:|
|
||||||
'|:--------|------------:|---------------:|'
|
| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |
|
||||||
'-------------------:|---------------:|---------:|-------:|\n'
|
| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |"""
|
||||||
'| ETH/BTC | 2 | 15.00 | '
|
|
||||||
'0.60000000 | 20.0 | 2 | 0 |\n'
|
|
||||||
'| TOTAL | 2 | 15.00 | '
|
|
||||||
'0.60000000 | 20.0 | 2 | 0 |'
|
|
||||||
)
|
)
|
||||||
|
#
|
||||||
|
# print()
|
||||||
|
# print(backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results))
|
||||||
|
#
|
||||||
|
# print()
|
||||||
|
# print()
|
||||||
|
# print(result_str)
|
||||||
|
|
||||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="no way of currently testing this")
|
||||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.start() method
|
Test Backtesting.start() method
|
||||||
@ -416,6 +426,40 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
assert log_has(line, caplog.record_tuples)
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.start() method if no data is found
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_timeframe(input1, input2):
|
||||||
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
|
||||||
|
mocker.patch('freqtrade.exchange.get_ticker_history')
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
backtest=MagicMock(),
|
||||||
|
_generate_text_table=MagicMock(return_value='1'),
|
||||||
|
get_timeframe=get_timeframe,
|
||||||
|
)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
conf['ticker_interval'] = "1m"
|
||||||
|
conf['live'] = False
|
||||||
|
conf['datadir'] = None
|
||||||
|
conf['export'] = None
|
||||||
|
conf['timerange'] = '20180101-20180102'
|
||||||
|
|
||||||
|
backtesting = Backtesting(conf)
|
||||||
|
backtesting.start()
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
|
||||||
|
assert log_has('No data found. Terminating.', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, fee, mocker) -> None:
|
def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test Backtesting.backtest() method
|
Test Backtesting.backtest() method
|
||||||
@ -435,6 +479,7 @@ def test_backtest(default_conf, fee, mocker) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
@ -457,6 +502,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_processed(default_conf, mocker) -> None:
|
def test_processed(default_conf, mocker) -> None:
|
||||||
@ -478,7 +524,7 @@ def test_processed(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 16]]
|
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres, mocker)
|
simple_backtest(default_conf, contour, numres, mocker)
|
||||||
|
|
||||||
@ -538,7 +584,10 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
|||||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
backtesting.populate_buy_trend = _trend_alternate # Override
|
||||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
backtesting.populate_sell_trend = _trend_alternate # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert len(results) == 3
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
|
# One trade was force-closed at the end
|
||||||
|
assert len(results.loc[results.open_at_end]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, fee, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
@ -550,22 +599,30 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
'freqtrade.optimize.backtesting.file_dump_json',
|
'freqtrade.optimize.backtesting.file_dump_json',
|
||||||
new=lambda n, r: (names.append(n), records.append(r))
|
new=lambda n, r: (names.append(n), records.append(r))
|
||||||
)
|
)
|
||||||
backtest_conf = _make_backtest_conf(
|
|
||||||
mocker,
|
|
||||||
conf=default_conf,
|
|
||||||
pair='UNITTEST/BTC',
|
|
||||||
record="trades"
|
|
||||||
)
|
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
|
||||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
"UNITTEST/BTC", "UNITTEST/BTC"],
|
||||||
results = backtesting.backtest(backtest_conf)
|
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
|
||||||
assert len(results) == 3
|
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
|
||||||
|
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 21, 36, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 12, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 44, 00).datetime],
|
||||||
|
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
||||||
|
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
||||||
|
"open_index": [1, 119, 153, 185],
|
||||||
|
"close_index": [118, 151, 184, 199],
|
||||||
|
"trade_duration": [123, 34, 31, 14]})
|
||||||
|
backtesting._store_backtest_result("backtest-result.json", results)
|
||||||
|
assert len(results) == 4
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
assert names == ['backtest-result.json']
|
assert names == ['backtest-result.json']
|
||||||
records = records[0]
|
records = records[0]
|
||||||
# Ensure records are of correct type
|
# Ensure records are of correct type
|
||||||
assert len(records) == 3
|
assert len(records) == 4
|
||||||
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||||
# Below follows just a typecheck of the schema/type of trade-records
|
# Below follows just a typecheck of the schema/type of trade-records
|
||||||
oix = None
|
oix = None
|
||||||
@ -582,6 +639,7 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
assert dur > 0
|
assert dur > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="no way of currently testing this")
|
||||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
@ -23,8 +23,6 @@ def init_hyperopt(default_conf, mocker):
|
|||||||
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
||||||
if not _HYPEROPT_INITIALIZED:
|
if not _HYPEROPT_INITIALIZED:
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
|
|
||||||
MagicMock(return_value=default_conf))
|
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
|
||||||
_HYPEROPT = Hyperopt(default_conf)
|
_HYPEROPT = Hyperopt(default_conf)
|
||||||
_HYPEROPT_INITIALIZED = True
|
_HYPEROPT_INITIALIZED = True
|
||||||
@ -63,9 +61,11 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
Test start() function
|
Test start() function
|
||||||
"""
|
"""
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
|
|
||||||
MagicMock(return_value=default_conf))
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
@ -123,6 +123,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="no way of currently testing this")
|
||||||
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
||||||
hyperopt = _HYPEROPT
|
hyperopt = _HYPEROPT
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
@ -135,7 +136,9 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert ' 1/2: foo. Loss 1.00000'in out
|
with capsys.disabled():
|
||||||
|
print("out is: {}".format(out))
|
||||||
|
assert ' 1/2: foo. Loss 1.00000'in out
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
||||||
@ -182,7 +185,6 @@ def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
@ -227,7 +229,6 @@ def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) ->
|
|||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
@ -253,7 +254,6 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.update({'config': 'config.json.example'})
|
conf.update({'config': 'config.json.example'})
|
||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'mongodb': False})
|
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
@ -270,7 +270,6 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, defa
|
|||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
@ -348,7 +347,6 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.update({'config': 'config.json.example'})
|
conf.update({'config': 'config.json.example'})
|
||||||
conf.update({'epochs': 1})
|
conf.update({'epochs': 1})
|
||||||
conf.update({'mongodb': False})
|
|
||||||
conf.update({'timerange': None})
|
conf.update({'timerange': None})
|
||||||
conf.update({'spaces': 'all'})
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
@ -360,35 +358,6 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
mock_fmin.assert_called_once()
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
|
||||||
mock_mongotrials = mocker.patch(
|
|
||||||
'freqtrade.optimize.hyperopt.MongoTrials',
|
|
||||||
return_value=create_trials(mocker)
|
|
||||||
)
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'epochs': 1})
|
|
||||||
conf.update({'mongodb': True})
|
|
||||||
conf.update({'timerange': None})
|
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
|
||||||
|
|
||||||
hyperopt = Hyperopt(conf)
|
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
|
||||||
|
|
||||||
hyperopt.start()
|
|
||||||
mock_mongotrials.assert_called_once()
|
|
||||||
mock_fmin.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
# test log_trials_result
|
|
||||||
# test buy_strategy_generator def populate_buy_trend
|
|
||||||
# test optimizer if 'ro_t1' in params
|
|
||||||
|
|
||||||
def test_format_results(init_hyperopt):
|
def test_format_results(init_hyperopt):
|
||||||
"""
|
"""
|
||||||
Test Hyperopt.format_results()
|
Test Hyperopt.format_results()
|
||||||
@ -400,7 +369,7 @@ def test_format_results(init_hyperopt):
|
|||||||
('LTC/BTC', 1, 1, 123),
|
('LTC/BTC', 1, 1, 123),
|
||||||
('XPR/BTC', -1, -2, -246)
|
('XPR/BTC', -1, -2, -246)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
result = _HYPEROPT.format_results(df)
|
result = _HYPEROPT.format_results(df)
|
||||||
@ -530,7 +499,7 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
trades = [
|
trades = [
|
||||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||||
]
|
]
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
|
||||||
|
|
||||||
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_optimize_conf():
|
|
||||||
hyperopt_conf = hyperopt_optimize_conf()
|
|
||||||
|
|
||||||
assert "max_open_trades" in hyperopt_conf
|
|
||||||
assert "stake_currency" in hyperopt_conf
|
|
||||||
assert "stake_amount" in hyperopt_conf
|
|
||||||
assert "minimal_roi" in hyperopt_conf
|
|
||||||
assert "stoploss" in hyperopt_conf
|
|
||||||
assert "bid_strategy" in hyperopt_conf
|
|
||||||
assert "exchange" in hyperopt_conf
|
|
||||||
assert "pair_whitelist" in hyperopt_conf['exchange']
|
|
@ -326,8 +326,6 @@ def test_load_tickerdata_file() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker) -> None:
|
def test_init(default_conf, mocker) -> None:
|
||||||
conf = {'exchange': {'pair_whitelist': []}}
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
|
|
||||||
assert {} == optimize.load_data(
|
assert {} == optimize.load_data(
|
||||||
'',
|
'',
|
||||||
pairs=[],
|
pairs=[],
|
||||||
|
@ -7,9 +7,11 @@ Unit test file for rpc/rpc.py
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||||
|
|
||||||
@ -29,7 +31,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -41,19 +43,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, result) = rpc.rpc_trade_status()
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
assert error
|
rpc._rpc_trade_status()
|
||||||
assert 'trader is not running' in result
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, result) = rpc.rpc_trade_status()
|
with pytest.raises(RPCException, match=r'.*no active trade*'):
|
||||||
assert error
|
rpc._rpc_trade_status()
|
||||||
assert 'no active trade' in result
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, result) = rpc.rpc_trade_status()
|
trades = rpc._rpc_trade_status()
|
||||||
assert not error
|
trade = trades[0]
|
||||||
trade = result[0]
|
|
||||||
|
|
||||||
result_message = [
|
result_message = [
|
||||||
'*Trade ID:* `1`\n'
|
'*Trade ID:* `1`\n'
|
||||||
@ -65,10 +64,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'*Close Rate:* `None`\n'
|
'*Close Rate:* `None`\n'
|
||||||
'*Current Rate:* `0.00001098`\n'
|
'*Current Rate:* `0.00001098`\n'
|
||||||
'*Close Profit:* `None`\n'
|
'*Close Profit:* `None`\n'
|
||||||
|
'*Stake Value:* `0.00099909`\n'
|
||||||
'*Current Profit:* `-0.59%`\n'
|
'*Current Profit:* `-0.59%`\n'
|
||||||
'*Open Order:* `(limit buy rem=0.00000000)`'
|
'*Open Order:* `(limit buy rem=0.00000000)`'
|
||||||
]
|
]
|
||||||
assert result == result_message
|
assert trades == result_message
|
||||||
assert trade.find('[ETH/BTC]') >= 0
|
assert trade.find('[ETH/BTC]') >= 0
|
||||||
|
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -90,20 +90,19 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, result) = rpc.rpc_status_table()
|
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
|
||||||
assert error
|
rpc._rpc_status_table()
|
||||||
assert '*Status:* `trader is not running`' in result
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, result) = rpc.rpc_status_table()
|
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
|
||||||
assert error
|
rpc._rpc_status_table()
|
||||||
assert '*Status:* `no active order`' in result
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, result) = rpc.rpc_status_table()
|
result = rpc._rpc_status_table()
|
||||||
assert 'just now' in result['Since'].all()
|
assert 'just now' in result['Since'].all()
|
||||||
assert '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()
|
||||||
|
assert 'Value' in result
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
@ -113,7 +112,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -140,8 +139,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
# Try valid data
|
# Try valid data
|
||||||
update.message.text = '/daily 2'
|
update.message.text = '/daily 2'
|
||||||
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||||
assert not error
|
|
||||||
assert len(days) == 7
|
assert len(days) == 7
|
||||||
for day in days:
|
for day in days:
|
||||||
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||||
@ -154,9 +152,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
assert str(days[0][0]) == str(datetime.utcnow().date())
|
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
|
||||||
assert error
|
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||||
assert days.find('must be an integer greater than 0') >= 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
@ -170,7 +167,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -184,9 +181,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
||||||
assert error
|
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert stats.find('no closed trade') >= 0
|
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
@ -219,8 +215,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert not error
|
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||||
@ -248,7 +243,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -281,8 +276,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
|
|||||||
for trade in Trade.query.order_by(Trade.id).all():
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
trade.open_rate = None
|
trade.open_rate = None
|
||||||
|
|
||||||
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
assert not error
|
|
||||||
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
assert prec_satoshi(stats['profit_closed_coin'], 0)
|
||||||
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
assert prec_satoshi(stats['profit_closed_percent'], 0)
|
||||||
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
||||||
@ -320,7 +314,7 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -330,18 +324,16 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||||
assert not error
|
assert prec_satoshi(total, 12)
|
||||||
(trade, x, y, z) = res
|
assert prec_satoshi(value, 180000)
|
||||||
assert prec_satoshi(x, 12)
|
assert 'USD' in symbol
|
||||||
assert prec_satoshi(z, 180000)
|
assert len(output) == 1
|
||||||
assert 'USD' in y
|
assert 'BTC' in output[0]['currency']
|
||||||
assert len(trade) == 1
|
assert prec_satoshi(output[0]['available'], 10)
|
||||||
assert 'BTC' in trade[0]['currency']
|
assert prec_satoshi(output[0]['balance'], 12)
|
||||||
assert prec_satoshi(trade[0]['available'], 10)
|
assert prec_satoshi(output[0]['pending'], 2)
|
||||||
assert prec_satoshi(trade[0]['balance'], 12)
|
assert prec_satoshi(output[0]['est_btc'], 12)
|
||||||
assert prec_satoshi(trade[0]['pending'], 2)
|
|
||||||
assert prec_satoshi(trade[0]['est_btc'], 12)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_start(mocker, default_conf) -> None:
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
@ -350,7 +342,7 @@ def test_rpc_start(mocker, default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -361,13 +353,11 @@ def test_rpc_start(mocker, default_conf) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
|
||||||
(error, result) = rpc.rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert not error
|
|
||||||
assert '`Starting trader ...`' in result
|
assert '`Starting trader ...`' in result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
(error, result) = rpc.rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert error
|
|
||||||
assert '*Status:* `already running`' in result
|
assert '*Status:* `already running`' in result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
@ -378,7 +368,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -389,13 +379,11 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
(error, result) = rpc.rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert not error
|
|
||||||
assert '`Stopping trader ...`' in result
|
assert '`Stopping trader ...`' in result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
(error, result) = rpc.rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert error
|
|
||||||
assert '*Status:* `already stopped`' in result
|
assert '*Status:* `already stopped`' in result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
@ -406,7 +394,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -428,36 +416,26 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == 'Invalid argument.'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
rpc._rpc_forcesell('1')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
(error, res) = rpc.rpc_forcesell(None)
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell(None)
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
(error, res) = rpc.rpc_forcesell('all')
|
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
||||||
assert error
|
rpc._rpc_forcesell('all')
|
||||||
assert res == '`trader is not running`'
|
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert cancel_order_mock.call_count == 0
|
assert cancel_order_mock.call_count == 0
|
||||||
@ -475,9 +453,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
)
|
)
|
||||||
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
# and trade amount is updated
|
# and trade amount is updated
|
||||||
(error, res) = rpc.rpc_forcesell('1')
|
rpc._rpc_forcesell('1')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert trade.amount == filled_amount
|
assert trade.amount == filled_amount
|
||||||
|
|
||||||
@ -495,9 +471,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
(error, res) = rpc.rpc_forcesell('2')
|
rpc._rpc_forcesell('2')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
assert trade.amount == amount
|
assert trade.amount == amount
|
||||||
|
|
||||||
@ -511,9 +485,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'side': 'sell'
|
'side': 'sell'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
(error, res) = rpc.rpc_forcesell('3')
|
rpc._rpc_forcesell('3')
|
||||||
assert not error
|
|
||||||
assert res == ''
|
|
||||||
# status quo, no exchange calls
|
# status quo, no exchange calls
|
||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
|
|
||||||
@ -525,7 +497,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -550,8 +522,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
(error, res) = rpc.rpc_performance()
|
res = rpc._rpc_performance()
|
||||||
assert not error
|
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]['pair'] == 'ETH/BTC'
|
assert res[0]['pair'] == 'ETH/BTC'
|
||||||
assert res[0]['count'] == 1
|
assert res[0]['count'] == 1
|
||||||
@ -564,7 +535,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
|||||||
"""
|
"""
|
||||||
patch_get_signal(mocker, (True, False))
|
patch_get_signal(mocker, (True, False))
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -576,14 +547,12 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
|
|||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
(error, trades) = rpc.rpc_count()
|
trades = rpc._rpc_count()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
assert not error
|
|
||||||
assert nb_trades == 0
|
assert nb_trades == 0
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
(error, trades) = rpc.rpc_count()
|
trades = rpc._rpc_count()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
assert not error
|
|
||||||
assert nb_trades == 1
|
assert nb_trades == 1
|
||||||
|
@ -7,49 +7,35 @@ from copy import deepcopy
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.rpc.rpc_manager import RPCManager
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
from freqtrade.rpc.telegram import Telegram
|
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_manager_object() -> None:
|
def test_rpc_manager_object() -> None:
|
||||||
"""
|
""" Test the Arguments object has the mandatory methods """
|
||||||
Test the Arguments object has the mandatory methods
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
assert hasattr(RPCManager, '_init')
|
|
||||||
assert hasattr(RPCManager, 'send_msg')
|
assert hasattr(RPCManager, 'send_msg')
|
||||||
assert hasattr(RPCManager, 'cleanup')
|
assert hasattr(RPCManager, 'cleanup')
|
||||||
|
|
||||||
|
|
||||||
def test__init__(mocker, default_conf) -> None:
|
def test__init__(mocker, default_conf) -> None:
|
||||||
"""
|
""" Test __init__() method """
|
||||||
Test __init__() method
|
conf = deepcopy(default_conf)
|
||||||
"""
|
conf['telegram']['enabled'] = False
|
||||||
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||||
assert rpc_manager.freqtrade == freqtradebot
|
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
assert rpc_manager.telegram is None
|
|
||||||
assert init_mock.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
""" Test _init() method with Telegram disabled """
|
||||||
Test _init() method with Telegram disabled
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
assert rpc_manager.telegram is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
|
||||||
|
|
||||||
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
len_modules = len(rpc_manager.registered_modules)
|
len_modules = len(rpc_manager.registered_modules)
|
||||||
assert len_modules == 1
|
assert len_modules == 1
|
||||||
assert 'telegram' in rpc_manager.registered_modules
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert isinstance(rpc_manager.telegram, Telegram)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
# Check we have Telegram as a registered modules
|
# Check we have Telegram as a registered modules
|
||||||
assert 'telegram' in rpc_manager.registered_modules
|
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
|
||||||
rpc_manager.cleanup()
|
rpc_manager.cleanup()
|
||||||
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
assert 'telegram' not in rpc_manager.registered_modules
|
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
assert log_has('test', caplog.record_tuples)
|
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
assert log_has('test', caplog.record_tuples)
|
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
@ -32,6 +32,9 @@ class DummyCls(Telegram):
|
|||||||
super().__init__(freqtrade)
|
super().__init__(freqtrade)
|
||||||
self.state = {'called': False}
|
self.state = {'called': False}
|
||||||
|
|
||||||
|
def _init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def dummy_handler(self, *args, **kwargs) -> None:
|
def dummy_handler(self, *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
@ -60,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog) -> None:
|
def test_init(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
""" Test _init() method """
|
||||||
Test _init() method
|
|
||||||
"""
|
|
||||||
start_polling = MagicMock()
|
start_polling = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||||
|
|
||||||
@ -70,31 +71,16 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.call_count == 0
|
assert start_polling.call_count == 0
|
||||||
|
|
||||||
# number of handles registered
|
# number of handles registered
|
||||||
assert start_polling.dispatcher.add_handler.call_count == 11
|
assert start_polling.dispatcher.add_handler.call_count > 0
|
||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
||||||
"['count'], ['help'], ['version']]"
|
"['count'], ['reload_conf'], ['help'], ['version']]"
|
||||||
|
|
||||||
assert log_has(message_str, caplog.record_tuples)
|
assert log_has(message_str, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_init_disabled(default_conf, mocker, caplog) -> None:
|
|
||||||
"""
|
|
||||||
Test _init() method when Telegram is disabled
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
|
|
||||||
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
|
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
|
|
||||||
"['count'], ['help'], ['version']]"
|
|
||||||
|
|
||||||
assert not log_has(message_str, caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup(default_conf, mocker) -> None:
|
def test_cleanup(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test cleanup() method
|
Test cleanup() method
|
||||||
@ -103,44 +89,11 @@ def test_cleanup(default_conf, mocker) -> None:
|
|||||||
updater_mock.stop = MagicMock()
|
updater_mock.stop = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
||||||
|
|
||||||
# not enabled
|
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
telegram.cleanup()
|
|
||||||
assert telegram._updater is None
|
|
||||||
assert updater_mock.call_count == 0
|
|
||||||
assert not hasattr(telegram._updater, 'stop')
|
|
||||||
assert updater_mock.stop.call_count == 0
|
|
||||||
|
|
||||||
# enabled
|
|
||||||
conf['telegram']['enabled'] = True
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
telegram.cleanup()
|
telegram.cleanup()
|
||||||
assert telegram._updater.stop.call_count == 1
|
assert telegram._updater.stop.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker) -> None:
|
|
||||||
"""
|
|
||||||
Test is_enabled() method
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
|
||||||
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
|
|
||||||
assert telegram.is_enabled()
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_not_enabled(default_conf, mocker) -> None:
|
|
||||||
"""
|
|
||||||
Test is_enabled() method
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
|
|
||||||
|
|
||||||
assert not telegram.is_enabled()
|
|
||||||
|
|
||||||
|
|
||||||
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test authorized_only() method when we are authorized
|
Test authorized_only() method when we are authorized
|
||||||
@ -256,9 +209,9 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])),
|
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -296,7 +249,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -341,7 +294,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -397,7 +350,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -465,7 +418,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -506,7 +459,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -604,7 +557,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
@ -634,7 +587,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
@ -656,7 +609,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -667,7 +620,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
|||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
telegram._start(bot=MagicMock(), update=update)
|
telegram._start(bot=MagicMock(), update=update)
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
assert msg_mock.call_count == 0
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||||
@ -680,7 +633,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -705,7 +658,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -730,7 +683,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
@ -745,6 +698,29 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||||
|
""" Test _reload_conf() method """
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
telegram._reload_conf(bot=MagicMock(), update=update)
|
||||||
|
assert freqtradebot.state == State.RELOAD_CONF
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
|
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test _forcesell() method
|
Test _forcesell() method
|
||||||
@ -875,7 +851,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
@ -917,7 +893,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
@ -958,7 +934,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
@ -981,7 +957,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.exchange',
|
'freqtrade.freqtradebot.exchange',
|
||||||
@ -1024,7 +1000,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
@ -1044,7 +1020,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
@ -1066,13 +1042,8 @@ def test_send_msg(default_conf, mocker) -> None:
|
|||||||
freqtradebot = FreqtradeBot(conf)
|
freqtradebot = FreqtradeBot(conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = False
|
|
||||||
telegram.send_msg('test', bot)
|
|
||||||
assert not bot.method_calls
|
|
||||||
bot.reset_mock()
|
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
telegram.send_msg('test', bot)
|
telegram._send_msg('test', bot)
|
||||||
assert len(bot.method_calls) == 1
|
assert len(bot.method_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
@ -1090,7 +1061,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
|||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
telegram.send_msg('test', bot)
|
telegram._send_msg('test', bot)
|
||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
||||||
|
@ -26,13 +26,30 @@ def test_load_strategy(result):
|
|||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_strategy_from_url(result):
|
||||||
|
resolver = StrategyResolver()
|
||||||
|
resolver._load_strategy('https://freq.isaac.international/'
|
||||||
|
'dev/strategies/GBPAQEFGGWCMWVFU34P'
|
||||||
|
'MVGS4P2NJR4IDFNVI4LTCZAKJAD3JCXUMBI4J/AverageStrategy/code')
|
||||||
|
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||||
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_custom_directory(result):
|
def test_load_strategy_custom_directory(result):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver()
|
||||||
extra_dir = os.path.join('some', 'path')
|
extra_dir = os.path.join('some', 'path')
|
||||||
with pytest.raises(
|
|
||||||
FileNotFoundError,
|
if os.name == 'nt':
|
||||||
match=r".*No such file or directory: '{}'".format(extra_dir)):
|
with pytest.raises(
|
||||||
resolver._load_strategy('TestStrategy', extra_dir)
|
FileNotFoundError,
|
||||||
|
match="FileNotFoundError: [WinError 3] The system cannot find the "
|
||||||
|
"path specified: '{}'".format(extra_dir)):
|
||||||
|
resolver._load_strategy('TestStrategy', extra_dir)
|
||||||
|
else:
|
||||||
|
with pytest.raises(
|
||||||
|
FileNotFoundError,
|
||||||
|
match=r".*No such file or directory: '{}'".format(extra_dir)):
|
||||||
|
resolver._load_strategy('TestStrategy', extra_dir)
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
@ -46,7 +46,7 @@ def test_analyze_object() -> None:
|
|||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
dataframe = Analyze.parse_ticker_dataframe(result)
|
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||||
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
assert len(result.index) == len(dataframe.index) # last partial candle NOT removed (for non binance or other known exchanges)
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
@ -188,4 +188,4 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
|||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
assert len(data['UNITTEST/BTC']) == 100 # partial candle was NOT removed (only for known exchanges like binance)
|
||||||
|
@ -13,6 +13,7 @@ from jsonschema import ValidationError
|
|||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
@ -140,6 +141,43 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
assert validated_conf.get('strategy_path') == '/some/path'
|
assert validated_conf.get('strategy_path') == '/some/path'
|
||||||
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
assert validated_conf.get('db_url') == 'sqlite:///someurl'
|
||||||
|
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = False
|
||||||
|
del conf["db_url"]
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
|
||||||
|
|
||||||
|
# Test dry=run with ProdURL
|
||||||
|
conf = default_conf.copy()
|
||||||
|
conf["dry_run"] = True
|
||||||
|
conf["db_url"] = DEFAULT_DB_PROD_URL
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path'
|
||||||
|
]
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
|
||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
"""
|
"""
|
||||||
@ -310,7 +348,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
arglist = [
|
arglist = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--epochs', '10',
|
'--epochs', '10',
|
||||||
'--use-mongodb',
|
|
||||||
'--spaces', 'all',
|
'--spaces', 'all',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -324,10 +361,6 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
|
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
|
||||||
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'mongodb' in config
|
|
||||||
assert config['mongodb'] is True
|
|
||||||
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
|
|
||||||
|
|
||||||
assert 'spaces' in config
|
assert 'spaces' in config
|
||||||
assert config['spaces'] == ['all']
|
assert config['spaces'] == ['all']
|
||||||
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
||||||
|
@ -16,7 +16,7 @@ def load_dataframe_pair(pairs):
|
|||||||
dataframe = ld[pairs[0]]
|
dataframe = ld[pairs[0]]
|
||||||
|
|
||||||
analyze = Analyze({'strategy': 'DefaultStrategy'})
|
analyze = Analyze({'strategy': 'DefaultStrategy'})
|
||||||
dataframe = analyze.analyze_ticker(dataframe)
|
dataframe = analyze.analyze_ticker(dataframe, pairs[0])
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +40,8 @@ def test_pair_convertion_object():
|
|||||||
assert pair_convertion.price == 30000.123
|
assert pair_convertion.price == 30000.123
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_is_supported():
|
def test_fiat_convert_is_supported(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
||||||
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
||||||
@ -48,7 +49,9 @@ def test_fiat_convert_is_supported():
|
|||||||
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_add_pair():
|
def test_fiat_convert_add_pair(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
pair_len = len(fiat_convert._pairs)
|
pair_len = len(fiat_convert._pairs)
|
||||||
@ -70,11 +73,8 @@ def test_fiat_convert_add_pair():
|
|||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_find_price(mocker):
|
def test_fiat_convert_find_price(mocker):
|
||||||
api_mock = MagicMock(return_value={
|
patch_coinmarketcap(mocker)
|
||||||
'price_usd': 12345.0,
|
|
||||||
'price_eur': 13000.2
|
|
||||||
})
|
|
||||||
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
||||||
@ -92,17 +92,15 @@ def test_fiat_convert_find_price(mocker):
|
|||||||
|
|
||||||
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.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
|
||||||
|
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
|
||||||
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_get_price(mocker):
|
def test_fiat_convert_get_price(mocker):
|
||||||
api_mock = MagicMock(return_value={
|
patch_coinmarketcap(mocker)
|
||||||
'price_usd': 28000.0,
|
|
||||||
'price_eur': 15000.0
|
|
||||||
})
|
|
||||||
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
@ -172,8 +170,9 @@ def test_fiat_init_network_exception(mocker):
|
|||||||
assert length_cryptomap == 0
|
assert length_cryptomap == 0
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_without_network():
|
def test_fiat_convert_without_network(mocker):
|
||||||
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
@ -186,6 +185,7 @@ def test_fiat_convert_without_network():
|
|||||||
|
|
||||||
|
|
||||||
def test_convert_amount(mocker):
|
def test_convert_amount(mocker):
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
@ -57,7 +57,7 @@ def patch_RPCManager(mocker) -> MagicMock:
|
|||||||
:param mocker: mocker to patch RPCManager class
|
:param mocker: mocker to patch RPCManager class
|
||||||
:return: RPCManager.send_msg MagicMock to track if this method is called
|
:return: RPCManager.send_msg MagicMock to track if this method is called
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
return rpc_mock
|
return rpc_mock
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ def test_freqtradebot_object() -> None:
|
|||||||
Test the FreqtradeBot object has the mandatory public methods
|
Test the FreqtradeBot object has the mandatory public methods
|
||||||
"""
|
"""
|
||||||
assert hasattr(FreqtradeBot, 'worker')
|
assert hasattr(FreqtradeBot, 'worker')
|
||||||
assert hasattr(FreqtradeBot, 'clean')
|
assert hasattr(FreqtradeBot, 'cleanup')
|
||||||
assert hasattr(FreqtradeBot, 'create_trade')
|
assert hasattr(FreqtradeBot, 'create_trade')
|
||||||
assert hasattr(FreqtradeBot, 'get_target_bid')
|
assert hasattr(FreqtradeBot, 'get_target_bid')
|
||||||
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
|
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
|
||||||
@ -93,7 +93,7 @@ def test_freqtradebot(mocker, default_conf) -> None:
|
|||||||
assert freqtrade.state is State.STOPPED
|
assert freqtrade.state is State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_clean(mocker, default_conf, caplog) -> None:
|
def test_cleanup(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test clean() method
|
Test clean() method
|
||||||
"""
|
"""
|
||||||
@ -101,11 +101,8 @@ def test_clean(mocker, default_conf, caplog) -> None:
|
|||||||
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
assert freqtrade.state == State.RUNNING
|
freqtrade.cleanup()
|
||||||
|
assert log_has('Cleaning up modules ...', caplog.record_tuples)
|
||||||
assert freqtrade.clean()
|
|
||||||
assert freqtrade.state == State.STOPPED
|
|
||||||
assert log_has('Stopping trader and cleaning up modules...', caplog.record_tuples)
|
|
||||||
assert mock_cleanup.call_count == 1
|
assert mock_cleanup.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -502,27 +499,45 @@ def test_balance_fully_ask_side(mocker) -> None:
|
|||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 0.0}})
|
param = {
|
||||||
|
'use_book_order': False,
|
||||||
|
'book_order_top': 6,
|
||||||
|
'ask_last_balance': 0.0,
|
||||||
|
'percent_from_top': 0
|
||||||
|
}
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker) -> None:
|
def test_balance_fully_last_side(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
param = {
|
||||||
|
'use_book_order': False,
|
||||||
|
'book_order_top': 6,
|
||||||
|
'ask_last_balance': 0.0,
|
||||||
|
'percent_from_top': 0
|
||||||
|
}
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker) -> None:
|
def test_balance_bigger_last_ask(mocker) -> None:
|
||||||
"""
|
"""
|
||||||
Test get_target_bid() method
|
Test get_target_bid() method
|
||||||
"""
|
"""
|
||||||
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
|
param = {
|
||||||
|
'use_book_order': False,
|
||||||
|
'book_order_top': 6,
|
||||||
|
'ask_last_balance': 0.0,
|
||||||
|
'percent_from_top': 0.00
|
||||||
|
}
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param})
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
@ -852,7 +867,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
|
|||||||
Trade.session.add(trade_buy)
|
Trade.session.add(trade_buy)
|
||||||
|
|
||||||
# check it does cancel buy orders over the time limit
|
# check it does cancel buy orders over the time limit
|
||||||
freqtrade.check_handle_timedout(600)
|
freqtrade.check_handle_timedout()
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||||
@ -893,7 +908,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
|||||||
Trade.session.add(trade_sell)
|
Trade.session.add(trade_sell)
|
||||||
|
|
||||||
# check it does cancel sell orders over the time limit
|
# check it does cancel sell orders over the time limit
|
||||||
freqtrade.check_handle_timedout(600)
|
freqtrade.check_handle_timedout()
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
assert trade_sell.is_open is True
|
assert trade_sell.is_open is True
|
||||||
@ -933,7 +948,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
|||||||
|
|
||||||
# check it does cancel buy orders over the time limit
|
# check it does cancel buy orders over the time limit
|
||||||
# note this is for a partially-complete buy order
|
# note this is for a partially-complete buy order
|
||||||
freqtrade.check_handle_timedout(600)
|
freqtrade.check_handle_timedout()
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||||
@ -984,7 +999,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
|
|||||||
'recent call last):\n.*'
|
'recent call last):\n.*'
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade.check_handle_timedout(600)
|
freqtrade.check_handle_timedout()
|
||||||
assert filter(regexp.match, caplog.record_tuples)
|
assert filter(regexp.match, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,12 +3,16 @@ Unit test file for main.py
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.main import main, set_loggers
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.main import main, set_loggers, reconfigure
|
||||||
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import log_has
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
@ -70,7 +74,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
_init_modules=MagicMock(),
|
_init_modules=MagicMock(),
|
||||||
worker=MagicMock(side_effect=Exception),
|
worker=MagicMock(side_effect=Exception),
|
||||||
clean=MagicMock(),
|
cleanup=MagicMock(),
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
@ -97,7 +101,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
_init_modules=MagicMock(),
|
_init_modules=MagicMock(),
|
||||||
worker=MagicMock(side_effect=KeyboardInterrupt),
|
worker=MagicMock(side_effect=KeyboardInterrupt),
|
||||||
clean=MagicMock(),
|
cleanup=MagicMock(),
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
@ -124,7 +128,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
_init_modules=MagicMock(),
|
_init_modules=MagicMock(),
|
||||||
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||||
clean=MagicMock(),
|
cleanup=MagicMock(),
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
@ -140,3 +144,69 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
|||||||
main(args)
|
main(args)
|
||||||
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
assert log_has('Oh snap!', caplog.record_tuples)
|
assert log_has('Oh snap!', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test main() function
|
||||||
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
|
"""
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(return_value=State.RELOAD_CONF),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
# Raise exception as side effect to avoid endless loop
|
||||||
|
reconfigure_mock = mocker.patch(
|
||||||
|
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
main(['-c', 'config.json.example'])
|
||||||
|
|
||||||
|
assert reconfigure_mock.call_count == 1
|
||||||
|
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_reconfigure(mocker, default_conf) -> None:
|
||||||
|
""" Test recreate() function """
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
_init_modules=MagicMock(),
|
||||||
|
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
|
||||||
|
cleanup=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
# Renew mock to return modified data
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['stake_amount'] += 1
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
# reconfigure should return a new instance
|
||||||
|
freqtrade2 = reconfigure(
|
||||||
|
freqtrade,
|
||||||
|
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify we have a new instance with the new config
|
||||||
|
assert freqtrade is not freqtrade2
|
||||||
|
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']
|
||||||
|
@ -425,6 +425,8 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
close_profit FLOAT,
|
close_profit FLOAT,
|
||||||
stake_amount FLOAT NOT NULL,
|
stake_amount FLOAT NOT NULL,
|
||||||
amount FLOAT,
|
amount FLOAT,
|
||||||
|
initial_stop_loss FLOAT,
|
||||||
|
max_rate FLOAT,
|
||||||
open_date DATETIME NOT NULL,
|
open_date DATETIME NOT NULL,
|
||||||
close_date DATETIME,
|
close_date DATETIME,
|
||||||
open_order_id VARCHAR,
|
open_order_id VARCHAR,
|
||||||
|
2
freqtrade/tests/testdata/ADA_BTC-1m.json
vendored
2
freqtrade/tests/testdata/ADA_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ADA_BTC-5m.json
vendored
2
freqtrade/tests/testdata/ADA_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/DASH_BTC-1m.json
vendored
2
freqtrade/tests/testdata/DASH_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/DASH_BTC-5m.json
vendored
2
freqtrade/tests/testdata/DASH_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ETC_BTC-1m.json
vendored
2
freqtrade/tests/testdata/ETC_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ETC_BTC-5m.json
vendored
2
freqtrade/tests/testdata/ETC_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ETH_BTC-1m.json
vendored
2
freqtrade/tests/testdata/ETH_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ETH_BTC-5m.json
vendored
2
freqtrade/tests/testdata/ETH_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/LTC_BTC-1m.json
vendored
2
freqtrade/tests/testdata/LTC_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/LTC_BTC-5m.json
vendored
2
freqtrade/tests/testdata/LTC_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/POWR_BTC-1m.json
vendored
2
freqtrade/tests/testdata/POWR_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/POWR_BTC-5m.json
vendored
2
freqtrade/tests/testdata/POWR_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/XLM_BTC-1m.json
vendored
2
freqtrade/tests/testdata/XLM_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/XLM_BTC-5m.json
vendored
2
freqtrade/tests/testdata/XLM_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/XMR_BTC-1m.json
vendored
2
freqtrade/tests/testdata/XMR_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/XMR_BTC-5m.json
vendored
2
freqtrade/tests/testdata/XMR_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ZEC_BTC-1m.json
vendored
2
freqtrade/tests/testdata/ZEC_BTC-1m.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/ZEC_BTC-5m.json
vendored
2
freqtrade/tests/testdata/ZEC_BTC-5m.json
vendored
File diff suppressed because one or more lines are too long
61
freqtrade/vendor/qtpylib/indicators.py
vendored
61
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -110,10 +110,13 @@ def heikinashi(bars):
|
|||||||
bars = bars.copy()
|
bars = bars.copy()
|
||||||
bars['ha_close'] = (bars['open'] + bars['high'] +
|
bars['ha_close'] = (bars['open'] + bars['high'] +
|
||||||
bars['low'] + bars['close']) / 4
|
bars['low'] + bars['close']) / 4
|
||||||
|
|
||||||
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
|
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
|
||||||
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
|
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
|
||||||
bars.loc[1:, 'ha_open'] = (
|
for x in range(2):
|
||||||
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
|
bars.loc[1:, 'ha_open'] = (
|
||||||
|
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
|
||||||
|
|
||||||
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
||||||
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
||||||
|
|
||||||
@ -248,45 +251,36 @@ def crossed_below(series1, series2):
|
|||||||
|
|
||||||
def rolling_std(series, window=200, min_periods=None):
|
def rolling_std(series, window=200, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
if min_periods == window and len(series) > window:
|
||||||
if min_periods == window:
|
return numpy_rolling_std(series, window, True)
|
||||||
return numpy_rolling_std(series, window, True)
|
else:
|
||||||
else:
|
try:
|
||||||
try:
|
return series.rolling(window=window, min_periods=min_periods).std()
|
||||||
return series.rolling(window=window, min_periods=min_periods).std()
|
except BaseException:
|
||||||
except BaseException:
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
|
||||||
except BaseException:
|
|
||||||
return pd.rolling_std(series, window=window, min_periods=min_periods)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def rolling_mean(series, window=200, min_periods=None):
|
def rolling_mean(series, window=200, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
if min_periods == window and len(series) > window:
|
||||||
if min_periods == window:
|
return numpy_rolling_mean(series, window, True)
|
||||||
return numpy_rolling_mean(series, window, True)
|
else:
|
||||||
else:
|
try:
|
||||||
try:
|
return series.rolling(window=window, min_periods=min_periods).mean()
|
||||||
return series.rolling(window=window, min_periods=min_periods).mean()
|
except BaseException:
|
||||||
except BaseException:
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
|
||||||
except BaseException:
|
|
||||||
return pd.rolling_mean(series, window=window, min_periods=min_periods)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def rolling_min(series, window=14, min_periods=None):
|
def rolling_min(series, window=14, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
try:
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
return series.rolling(window=window, min_periods=min_periods).min()
|
|
||||||
except BaseException:
|
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
@ -294,12 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
|
|||||||
def rolling_max(series, window=14, min_periods=None):
|
def rolling_max(series, window=14, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
try:
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
return series.rolling(window=window, min_periods=min_periods).min()
|
|
||||||
except BaseException:
|
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
@ -566,9 +557,9 @@ def stoch(df, window=14, d=3, k=3, fast=False):
|
|||||||
|
|
||||||
return pd.DataFrame(index=df.index, data=data)
|
return pd.DataFrame(index=df.index, data=data)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def zscore(bars, window=20, stds=1, col='close'):
|
def zscore(bars, window=20, stds=1, col='close'):
|
||||||
""" get zscore of price """
|
""" get zscore of price """
|
||||||
std = numpy_rolling_std(bars[col], window)
|
std = numpy_rolling_std(bars[col], window)
|
||||||
|
BIN
lib/libta_lib.a
Normal file
BIN
lib/libta_lib.a
Normal file
Binary file not shown.
35
lib/libta_lib.la
Executable file
35
lib/libta_lib.la
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
# libta_lib.la - a libtool library file
|
||||||
|
# Generated by ltmain.sh - GNU libtool 1.5.22 Debian 1.5.22-4 (1.1220.2.365 2005/12/18 22:14:06)
|
||||||
|
#
|
||||||
|
# Please DO NOT delete this file!
|
||||||
|
# It is necessary for linking the library.
|
||||||
|
|
||||||
|
# The name that we can dlopen(3).
|
||||||
|
dlname='libta_lib.so.0'
|
||||||
|
|
||||||
|
# Names of this library.
|
||||||
|
library_names='libta_lib.so.0.0.0 libta_lib.so.0 libta_lib.so'
|
||||||
|
|
||||||
|
# The name of the static archive.
|
||||||
|
old_library='libta_lib.a'
|
||||||
|
|
||||||
|
# Libraries that this one depends upon.
|
||||||
|
dependency_libs=' -lpthread -ldl'
|
||||||
|
|
||||||
|
# Version information for libta_lib.
|
||||||
|
current=0
|
||||||
|
age=0
|
||||||
|
revision=0
|
||||||
|
|
||||||
|
# Is this an already installed library?
|
||||||
|
installed=yes
|
||||||
|
|
||||||
|
# Should we warn about portability when linking against -modules?
|
||||||
|
shouldnotlink=no
|
||||||
|
|
||||||
|
# Files to dlopen/dlpreopen
|
||||||
|
dlopen=''
|
||||||
|
dlpreopen=''
|
||||||
|
|
||||||
|
# Directory that this library needs to be installed in:
|
||||||
|
libdir='/usr/local/lib'
|
1
lib/libta_lib.so.0
Symbolic link
1
lib/libta_lib.so.0
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
libta_lib.so.0.0.0
|
BIN
lib/libta_lib.so.0.0.0
Executable file
BIN
lib/libta_lib.so.0.0.0
Executable file
Binary file not shown.
@ -1,25 +1,26 @@
|
|||||||
ccxt==1.14.169
|
ccxt==1.14.224
|
||||||
SQLAlchemy==1.2.8
|
SQLAlchemy==1.2.8
|
||||||
python-telegram-bot==10.1.0
|
python-telegram-bot==10.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
cachetools==2.1.0
|
cachetools==2.1.0
|
||||||
requests==2.18.4
|
requests==2.19.1
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.23.0
|
pandas==0.23.1
|
||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.1
|
||||||
scipy==1.1.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.14.4
|
numpy==1.14.5
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
pytest==3.6.1
|
pytest==3.6.1
|
||||||
pytest-mock==1.10.0
|
pytest-mock==1.10.0
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
hyperopt==0.1
|
hyperopt==0.1
|
||||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||||
networkx==1.11 # pyup: ignore
|
networkx==1.11
|
||||||
|
git+git://github.com/berlinguyinca/technical
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
simplejson==3.15.0
|
||||||
# Required for plotting data
|
# Required for plotting data
|
||||||
#plotly==2.3.0
|
#plotly==2.3.0
|
||||||
|
@ -30,20 +30,27 @@ if not os.path.isfile(pairs_file):
|
|||||||
with open(pairs_file) as file:
|
with open(pairs_file) as file:
|
||||||
PAIRS = list(set(json.load(file)))
|
PAIRS = list(set(json.load(file)))
|
||||||
|
|
||||||
|
PAIRS.sort()
|
||||||
|
|
||||||
since_time = None
|
since_time = None
|
||||||
if args.days:
|
if args.days:
|
||||||
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
||||||
|
|
||||||
|
|
||||||
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._API = exchange.init_ccxt({'key': '',
|
exchange._API = exchange.init_ccxt({'key': '',
|
||||||
'secret': '',
|
'secret': '',
|
||||||
'name': args.exchange})
|
'name': args.exchange})
|
||||||
|
pairs_not_available = []
|
||||||
|
# Make sure API markets is initialized
|
||||||
|
exchange._API.load_markets()
|
||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
|
if pair not in exchange._API.markets:
|
||||||
|
pairs_not_available.append(pair)
|
||||||
|
print(f"skipping pair {pair}")
|
||||||
|
continue
|
||||||
for tick_interval in timeframes:
|
for tick_interval in timeframes:
|
||||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||||
|
|
||||||
@ -60,3 +67,7 @@ for pair in PAIRS:
|
|||||||
pair_print = pair.replace('/', '_')
|
pair_print = pair.replace('/', '_')
|
||||||
filename = f'{pair_print}-{tick_interval}.json'
|
filename = f'{pair_print}-{tick_interval}.json'
|
||||||
misc.file_dump_json(os.path.join(dl_path, filename), data)
|
misc.file_dump_json(os.path.join(dl_path, filename), data)
|
||||||
|
|
||||||
|
|
||||||
|
if pairs_not_available:
|
||||||
|
print(f"Pairs [{','.join(pairs_not_available)}] not availble.")
|
||||||
|
@ -5,45 +5,386 @@ Script to display when the bot will buy a specific pair
|
|||||||
Mandatory Cli parameters:
|
Mandatory Cli parameters:
|
||||||
-p / --pair: pair to examine
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
Option but recommended
|
|
||||||
-s / --strategy: strategy to use
|
|
||||||
|
|
||||||
|
|
||||||
Optional Cli parameters
|
Optional Cli parameters
|
||||||
|
-s / --strategy: strategy to use
|
||||||
-d / --datadir: path to pair backtest data
|
-d / --datadir: path to pair backtest data
|
||||||
--timerange: specify what timerange of data to use.
|
--timerange: specify what timerange of data to use.
|
||||||
-l / --live: Live, to download the latest ticker for the pair
|
-l / --live: Live, to download the latest ticker for the pair
|
||||||
-db / --db-url: Show trades stored in database
|
-db / --db-url: Show trades stored in database
|
||||||
|
|
||||||
|
--plot-max-ticks N: plot N data points and overwrite the internal 750 cut of
|
||||||
|
|
||||||
Indicators recommended
|
|
||||||
Row 1: sma, ema3, ema5, ema10, ema50
|
|
||||||
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
|
|
||||||
|
|
||||||
Example of usage:
|
Plotting Subplots, require the name of the dataframe column.
|
||||||
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
|
||||||
--indicators2 fastk,fastd
|
Each plot will be displayed as usual on exchanges
|
||||||
|
|
||||||
|
--plot-rsi <RSI>
|
||||||
|
--plot-cci <CCI>
|
||||||
|
--plot-osc <CCI>
|
||||||
|
--plot-macd <MACD>
|
||||||
|
--plot-cmf <CMF>
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Dict, List, Any
|
from typing import List
|
||||||
|
|
||||||
import plotly.graph_objs as go
|
import plotly.graph_objs as go
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
|
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade import persistence
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.optimize.backtesting import setup_configuration
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade import exchange
|
||||||
|
import freqtrade.optimize as optimize
|
||||||
|
from freqtrade import persistence
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_CONF: Dict[str, Any] = {}
|
_CONF: Dict[str, Any] = {}
|
||||||
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
|
|
||||||
|
def plot_dataframes_markers(data, fig, args):
|
||||||
|
"""
|
||||||
|
plots additional dataframe markers in the main plot
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args.plotdataframemarker:
|
||||||
|
for x in args.plotdataframemarker:
|
||||||
|
filter = data[(data[x] == 100) | (data[x] == -100)]
|
||||||
|
marker = go.Scatter(
|
||||||
|
x=filter.date,
|
||||||
|
y=filter.low * 0.99,
|
||||||
|
mode='markers',
|
||||||
|
name=x,
|
||||||
|
marker=dict(
|
||||||
|
symbol='diamond-tall-open',
|
||||||
|
size=10,
|
||||||
|
line=dict(width=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.append_trace(marker, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_dataframes(data, fig, args):
|
||||||
|
"""
|
||||||
|
plots additional dataframes in the main plot
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args.plotdataframe:
|
||||||
|
for x in args.plotdataframe:
|
||||||
|
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
|
||||||
|
fig.append_trace(chart, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_volume_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
adds the plotting of the volume
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
|
||||||
|
fig.append_trace(volume, plotnumber, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_macd_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
adds the plotting of the MACD if specified
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
macd = go.Scattergl(x=data['date'], y=data[args.plotmacd], name='MACD')
|
||||||
|
macdsignal = go.Scattergl(x=data['date'], y=data[args.plotmacd + 'signal'], name='MACD signal')
|
||||||
|
fig.append_trace(macd, plotnumber, 1)
|
||||||
|
fig.append_trace(macdsignal, plotnumber, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_rsi_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
|
||||||
|
this function plots an additional RSI chart under the exiting charts
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if args.plotrsi:
|
||||||
|
for x in args.plotrsi:
|
||||||
|
rsi = go.Scattergl(x=data['date'], y=data[x], name=x)
|
||||||
|
fig.append_trace(rsi, plotnumber, 1)
|
||||||
|
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'fillcolor': 'red',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 70,
|
||||||
|
'y1': 100,
|
||||||
|
'line': {'color': 'gray'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'fillcolor': 'green',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 0,
|
||||||
|
'y1': 30,
|
||||||
|
'line': {'color': 'gray'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_osc_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
|
||||||
|
this function plots an additional cci chart under the exiting charts
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args.plotosc:
|
||||||
|
for x in args.plotosc:
|
||||||
|
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
|
||||||
|
fig.append_trace(chart, plotnumber, 1)
|
||||||
|
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'fillcolor': 'gray',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 0.3,
|
||||||
|
'y1': 0.7,
|
||||||
|
'line': {'color': 'gray'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'type': 'line',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 0.6,
|
||||||
|
'y1': 0.6,
|
||||||
|
'line': {'color': 'red','width': 1}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'type': 'line',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 0.4,
|
||||||
|
'y1': 0.4,
|
||||||
|
'line': {'color': 'green','width':1}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_cmf_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
|
||||||
|
this function plots an additional cci chart under the exiting charts
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
minValue = 0;
|
||||||
|
maxValue = 0;
|
||||||
|
if args.plotcmf:
|
||||||
|
for x in args.plotcmf:
|
||||||
|
chart = go.Bar(x=data['date'], y=data[x], name=x)
|
||||||
|
fig.append_trace(chart, plotnumber, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_cci_dataframe(data, fig, args, plotnumber):
|
||||||
|
"""
|
||||||
|
|
||||||
|
this function plots an additional cci chart under the exiting charts
|
||||||
|
:param data:
|
||||||
|
:param fig:
|
||||||
|
:param args:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
minValue = 0;
|
||||||
|
maxValue = 0;
|
||||||
|
if args.plotcci:
|
||||||
|
for x in args.plotcci:
|
||||||
|
if minValue > min(data[x]):
|
||||||
|
minValue = min(data[x])
|
||||||
|
if maxValue < max(data[x]):
|
||||||
|
maxValue = max(data[x])
|
||||||
|
|
||||||
|
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
|
||||||
|
fig.append_trace(chart, plotnumber, 1)
|
||||||
|
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'fillcolor': 'red',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': 100,
|
||||||
|
'y1': maxValue,
|
||||||
|
'line': {'color': 'gray'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'yref': 'y' + str(plotnumber),
|
||||||
|
'fillcolor': 'green',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': DataFrame.min(data['date']),
|
||||||
|
'x1': DataFrame.max(data['date']),
|
||||||
|
'y0': -100,
|
||||||
|
'y1': minValue,
|
||||||
|
'line': {'color': 'gray'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_stop_loss_trade(df_sell, fig, analyze, args):
|
||||||
|
"""
|
||||||
|
plots the stop loss for the associated trades and buys
|
||||||
|
as well as the estimated profit ranges.
|
||||||
|
|
||||||
|
will be enabled if --stop-loss is provided
|
||||||
|
as argument
|
||||||
|
|
||||||
|
:param data:
|
||||||
|
:param trades:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
if args.stoplossdisplay is False:
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'associated_buy_price' not in df_sell:
|
||||||
|
return
|
||||||
|
|
||||||
|
stoploss = analyze.strategy.stoploss
|
||||||
|
|
||||||
|
for index, x in df_sell.iterrows():
|
||||||
|
if x['associated_buy_price'] > 0:
|
||||||
|
# draw stop loss
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'fillcolor': 'red',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': x['associated_buy_date'],
|
||||||
|
'x1': x['date'],
|
||||||
|
'y0': x['associated_buy_price'],
|
||||||
|
'y1': (x['associated_buy_price'] - abs(stoploss) * x['associated_buy_price']),
|
||||||
|
'line': {'color': 'red'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
totalTime = 0
|
||||||
|
for time in analyze.strategy.minimal_roi:
|
||||||
|
t = int(time)
|
||||||
|
totalTime = t + totalTime
|
||||||
|
|
||||||
|
enddate = x['date']
|
||||||
|
|
||||||
|
date = x['associated_buy_date'] + datetime.timedelta(minutes=totalTime)
|
||||||
|
|
||||||
|
# draw profit range
|
||||||
|
fig['layout']['shapes'].append(
|
||||||
|
{
|
||||||
|
'fillcolor': 'green',
|
||||||
|
'opacity': 0.1,
|
||||||
|
'type': 'rect',
|
||||||
|
'x0': date,
|
||||||
|
'x1': enddate,
|
||||||
|
'y0': x['associated_buy_price'],
|
||||||
|
'y1': x['associated_buy_price'] + x['associated_buy_price'] * analyze.strategy.minimal_roi[
|
||||||
|
time],
|
||||||
|
'line': {'color': 'green'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_profits(data):
|
||||||
|
"""
|
||||||
|
finds the profits between sells and the associated buys. This does not take in account
|
||||||
|
ROI!
|
||||||
|
:param data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# go over all the sells
|
||||||
|
# find all previous buys
|
||||||
|
|
||||||
|
df_sell = data[data['sell'] == 1]
|
||||||
|
df_sell['profit'] = 0
|
||||||
|
df_buys = data[data['buy'] == 1]
|
||||||
|
lastDate = data['date'].iloc[0]
|
||||||
|
|
||||||
|
for index, row in df_sell.iterrows():
|
||||||
|
|
||||||
|
buys = df_buys[(df_buys['date'] < row['date']) & (df_buys['date'] > lastDate)]
|
||||||
|
|
||||||
|
profit = None
|
||||||
|
if buys['date'].count() > 0:
|
||||||
|
buys = buys.tail()
|
||||||
|
profit = round(row['close'] / buys['close'].values[0] * 100 - 100, 2)
|
||||||
|
lastDate = row['date']
|
||||||
|
|
||||||
|
df_sell.loc[index, 'associated_buy_date'] = buys['date'].values[0]
|
||||||
|
df_sell.loc[index, 'associated_buy_price'] = buys['close'].values[0]
|
||||||
|
|
||||||
|
df_sell.loc[index, 'profit'] = profit
|
||||||
|
|
||||||
|
return df_sell
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args: Namespace) -> None:
|
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
@ -51,29 +392,14 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
Calls analyze() and plots the returned dataframe
|
Calls analyze() and plots the returned dataframe
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _CONF
|
pair = args.pair.replace('-', '_')
|
||||||
|
|
||||||
# Load the configuration
|
|
||||||
_CONF.update(setup_configuration(args))
|
|
||||||
|
|
||||||
# Set the pair to audit
|
|
||||||
pair = args.pair
|
|
||||||
|
|
||||||
if pair is None:
|
|
||||||
logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC')
|
|
||||||
exit()
|
|
||||||
|
|
||||||
if '/' not in pair:
|
|
||||||
logger.critical('--pair format must be XXX/YYY')
|
|
||||||
exit()
|
|
||||||
|
|
||||||
# Set timerange to use
|
|
||||||
timerange = Arguments.parse_timerange(args.timerange)
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
# Load the strategy
|
# Init strategy
|
||||||
try:
|
try:
|
||||||
analyze = Analyze(_CONF)
|
config = Configuration(args)
|
||||||
exchange.init(_CONF)
|
|
||||||
|
analyze = Analyze(config.get_config())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
@ -81,75 +407,40 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
)
|
)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
# Set the ticker to use
|
tick_interval = analyze.strategy.ticker_interval
|
||||||
tick_interval = analyze.get_ticker_interval()
|
|
||||||
|
|
||||||
# Load pair tickers
|
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pair.')
|
||||||
|
# Init Bittrex to use public API
|
||||||
|
exchange.init({'key': '', 'secret': ''})
|
||||||
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(
|
tickers = optimize.load_data(
|
||||||
datadir=args.datadir,
|
datadir=_CONF.get("datadir"),
|
||||||
pairs=[pair],
|
pairs=[pair],
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
refresh_pairs=False,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
# No ticker found, or impossible to download
|
|
||||||
if tickers == {}:
|
|
||||||
exit()
|
|
||||||
|
|
||||||
# Get trades already made from the DB
|
|
||||||
trades: List[Trade] = []
|
|
||||||
if args.db_url:
|
|
||||||
persistence.init(_CONF)
|
|
||||||
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
|
||||||
|
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
dataframe = dataframes[pair]
|
dataframe = dataframes[pair]
|
||||||
dataframe = analyze.populate_buy_trend(dataframe)
|
dataframe = analyze.populate_buy_trend(dataframe)
|
||||||
dataframe = analyze.populate_sell_trend(dataframe)
|
dataframe = analyze.populate_sell_trend(dataframe)
|
||||||
|
|
||||||
|
if len(dataframe.index) > args.plotticks:
|
||||||
|
logger.warning('Ticker contained more than {} candles, clipping.'.format(args.plotticks))
|
||||||
|
data = dataframe.tail(args.plotticks)
|
||||||
|
trades = []
|
||||||
|
if args.db_url:
|
||||||
|
engine = create_engine('sqlite:///' + args.db_url)
|
||||||
|
persistence.init(_CONF, engine)
|
||||||
|
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
||||||
|
|
||||||
if len(dataframe.index) > 750:
|
if len(dataframe.index) > 750:
|
||||||
logger.warning('Ticker contained more than 750 candles, clipping.')
|
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||||
|
data = dataframe.tail(750)
|
||||||
|
|
||||||
fig = generate_graph(
|
|
||||||
pair=pair,
|
|
||||||
trades=trades,
|
|
||||||
data=dataframe.tail(750),
|
|
||||||
args=args
|
|
||||||
)
|
|
||||||
|
|
||||||
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html'))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|
||||||
"""
|
|
||||||
Generate the graph from the data generated by Backtesting or from DB
|
|
||||||
:param pair: Pair to Display on the graph
|
|
||||||
:param trades: All trades created
|
|
||||||
:param data: Dataframe
|
|
||||||
:param args: sys.argv that contrains the two params indicators1, and indicators2
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Define the graph
|
|
||||||
fig = tools.make_subplots(
|
|
||||||
rows=3,
|
|
||||||
cols=1,
|
|
||||||
shared_xaxes=True,
|
|
||||||
row_width=[1, 1, 4],
|
|
||||||
vertical_spacing=0.0001,
|
|
||||||
)
|
|
||||||
fig['layout'].update(title=pair)
|
|
||||||
fig['layout']['yaxis1'].update(title='Price')
|
|
||||||
fig['layout']['yaxis2'].update(title='Volume')
|
|
||||||
fig['layout']['yaxis3'].update(title='Other')
|
|
||||||
|
|
||||||
# Common information
|
|
||||||
candles = go.Candlestick(
|
candles = go.Candlestick(
|
||||||
x=data.date,
|
x=data.date,
|
||||||
open=data.open,
|
open=data.open,
|
||||||
@ -160,6 +451,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
)
|
)
|
||||||
|
|
||||||
df_buy = data[data['buy'] == 1]
|
df_buy = data[data['buy'] == 1]
|
||||||
|
|
||||||
buys = go.Scattergl(
|
buys = go.Scattergl(
|
||||||
x=df_buy.date,
|
x=df_buy.date,
|
||||||
y=df_buy.close,
|
y=df_buy.close,
|
||||||
@ -167,23 +459,27 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
name='buy',
|
name='buy',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='triangle-up-dot',
|
symbol='triangle-up-dot',
|
||||||
size=9,
|
size=15,
|
||||||
line=dict(width=1),
|
line=dict(width=1),
|
||||||
color='green',
|
color='green',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
df_sell = data[data['sell'] == 1]
|
df_sell = find_profits(data)
|
||||||
sells = go.Scattergl(
|
|
||||||
|
sells = go.Scatter(
|
||||||
x=df_sell.date,
|
x=df_sell.date,
|
||||||
y=df_sell.close,
|
y=df_sell.close,
|
||||||
mode='markers',
|
mode='markers+text',
|
||||||
name='sell',
|
name='sell',
|
||||||
|
text=df_sell.profit,
|
||||||
|
textposition='top right',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
symbol='triangle-down-dot',
|
symbol='triangle-down-dot',
|
||||||
size=9,
|
size=15,
|
||||||
line=dict(width=1),
|
line=dict(width=1),
|
||||||
color='red',
|
color='red',
|
||||||
)
|
)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
trade_buys = go.Scattergl(
|
trade_buys = go.Scattergl(
|
||||||
@ -211,67 +507,107 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Row 1
|
bb_lower = go.Scatter(
|
||||||
|
x=data.date,
|
||||||
|
y=data.bb_lowerband,
|
||||||
|
name='BB lower',
|
||||||
|
line={'color': "transparent"},
|
||||||
|
)
|
||||||
|
bb_upper = go.Scatter(
|
||||||
|
x=data.date,
|
||||||
|
y=data.bb_upperband,
|
||||||
|
name='BB upper',
|
||||||
|
fill="tonexty",
|
||||||
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
|
line={'color': "transparent"},
|
||||||
|
)
|
||||||
|
bb_middle = go.Scatter(
|
||||||
|
x=data.date,
|
||||||
|
y=data.bb_middleband,
|
||||||
|
name='BB middle',
|
||||||
|
fill="tonexty",
|
||||||
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
|
line={'color': "red"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ugly hack for now
|
||||||
|
rowWidth = [1]
|
||||||
|
if args.plotvolume:
|
||||||
|
rowWidth.append(1)
|
||||||
|
if args.plotmacd:
|
||||||
|
rowWidth.append(1)
|
||||||
|
if args.plotrsi:
|
||||||
|
rowWidth.append(1)
|
||||||
|
if args.plotcci:
|
||||||
|
rowWidth.append(1)
|
||||||
|
if args.plotcmf:
|
||||||
|
rowWidth.append(1)
|
||||||
|
if args.plotosc:
|
||||||
|
rowWidth.append(1)
|
||||||
|
|
||||||
|
# standard layout signal + volume
|
||||||
|
fig = tools.make_subplots(
|
||||||
|
rows=len(rowWidth),
|
||||||
|
cols=1,
|
||||||
|
shared_xaxes=True,
|
||||||
|
row_width=rowWidth,
|
||||||
|
vertical_spacing=0.0001,
|
||||||
|
)
|
||||||
|
|
||||||
|
# todo should be optional
|
||||||
fig.append_trace(candles, 1, 1)
|
fig.append_trace(candles, 1, 1)
|
||||||
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
|
fig.append_trace(bb_middle, 1, 1)
|
||||||
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
|
|
||||||
if 'bb_lowerband' in data and 'bb_upperband' in data:
|
|
||||||
bb_lower = go.Scatter(
|
|
||||||
x=data.date,
|
|
||||||
y=data.bb_lowerband,
|
|
||||||
name='BB lower',
|
|
||||||
line={'color': "transparent"},
|
|
||||||
)
|
|
||||||
bb_upper = go.Scatter(
|
|
||||||
x=data.date,
|
|
||||||
y=data.bb_upperband,
|
|
||||||
name='BB upper',
|
|
||||||
fill="tonexty",
|
|
||||||
fillcolor="rgba(0,176,246,0.2)",
|
|
||||||
line={'color': "transparent"},
|
|
||||||
)
|
|
||||||
fig.append_trace(bb_lower, 1, 1)
|
|
||||||
fig.append_trace(bb_upper, 1, 1)
|
|
||||||
|
|
||||||
fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data)
|
|
||||||
fig.append_trace(buys, 1, 1)
|
fig.append_trace(buys, 1, 1)
|
||||||
fig.append_trace(sells, 1, 1)
|
fig.append_trace(sells, 1, 1)
|
||||||
fig.append_trace(trade_buys, 1, 1)
|
|
||||||
fig.append_trace(trade_sells, 1, 1)
|
|
||||||
|
|
||||||
# Row 2
|
# append stop loss/profit
|
||||||
volume = go.Bar(
|
plot_stop_loss_trade(df_sell, fig, analyze, args)
|
||||||
x=data['date'],
|
|
||||||
y=data['volume'],
|
|
||||||
name='Volume'
|
|
||||||
)
|
|
||||||
fig.append_trace(volume, 2, 1)
|
|
||||||
|
|
||||||
# Row 3
|
# plot other dataframes
|
||||||
fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data)
|
plot_dataframes(data, fig, args)
|
||||||
|
plot_dataframes_markers(data, fig, args)
|
||||||
|
|
||||||
return fig
|
fig['layout'].update(title=args.pair)
|
||||||
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
|
|
||||||
|
subplots = 1
|
||||||
|
|
||||||
def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots:
|
if args.plotvolume:
|
||||||
"""
|
subplots = subplots + 1
|
||||||
Generator all the indicator selected by the user for a specific row
|
plot_volume_dataframe(data, fig, args, subplots)
|
||||||
"""
|
fig['layout']['yaxis' + str(subplots)].update(title='Volume')
|
||||||
for indicator in raw_indicators.split(','):
|
|
||||||
if indicator in data:
|
|
||||||
scattergl = go.Scattergl(
|
|
||||||
x=data['date'],
|
|
||||||
y=data[indicator],
|
|
||||||
name=indicator
|
|
||||||
)
|
|
||||||
fig.append_trace(scattergl, row, 1)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
'Indicator "%s" ignored. Reason: This indicator is not found '
|
|
||||||
'in your strategy.',
|
|
||||||
indicator
|
|
||||||
)
|
|
||||||
|
|
||||||
return fig
|
if args.plotmacd:
|
||||||
|
subplots = subplots + 1
|
||||||
|
plot_macd_dataframe(data, fig, args, subplots)
|
||||||
|
fig['layout']['yaxis' + str(subplots)].update(title='MACD')
|
||||||
|
|
||||||
|
if args.plotrsi:
|
||||||
|
subplots = subplots + 1
|
||||||
|
plot_rsi_dataframe(data, fig, args, subplots)
|
||||||
|
fig['layout']['yaxis' + str(subplots)].update(title='RSI', range=[0, 100])
|
||||||
|
|
||||||
|
if args.plotcci:
|
||||||
|
subplots = subplots + 1
|
||||||
|
plot_cci_dataframe(data, fig, args, subplots)
|
||||||
|
fig['layout']['yaxis' + str(subplots)].update(title='CCI')
|
||||||
|
|
||||||
|
if args.plotosc:
|
||||||
|
subplots = subplots + 1
|
||||||
|
plot_osc_dataframe(data, fig, args, subplots)
|
||||||
|
fig['layout']['yaxis' + str(subplots)].update(title='OSC')
|
||||||
|
|
||||||
|
if args.plotcmf:
|
||||||
|
subplots = subplots + 1
|
||||||
|
plot_cmf_dataframe(data, fig, args, subplots)
|
||||||
|
fig['layout']['yaxis' + str(subplots)].update(title='CMF')
|
||||||
|
|
||||||
|
# updated all the
|
||||||
|
|
||||||
|
plot(fig, filename='freqtrade-plot.html')
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args: List[str]) -> Namespace:
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
@ -282,24 +618,6 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
|||||||
"""
|
"""
|
||||||
arguments = Arguments(args, 'Graph dataframe')
|
arguments = Arguments(args, 'Graph dataframe')
|
||||||
arguments.scripts_options()
|
arguments.scripts_options()
|
||||||
arguments.parser.add_argument(
|
|
||||||
'--indicators1',
|
|
||||||
help='Set indicators from your strategy you want in the first row of the graph. Separate '
|
|
||||||
'them with a coma. E.g: ema3,ema5 (default: %(default)s)',
|
|
||||||
type=str,
|
|
||||||
default='sma,ema3,ema5',
|
|
||||||
dest='indicators1',
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments.parser.add_argument(
|
|
||||||
'--indicators2',
|
|
||||||
help='Set indicators from your strategy you want in the third row of the graph. Separate '
|
|
||||||
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
|
|
||||||
type=str,
|
|
||||||
default='macd',
|
|
||||||
dest='indicators2',
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments.common_args_parser()
|
arguments.common_args_parser()
|
||||||
arguments.optimizer_shared_options(arguments.parser)
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
arguments.backtesting_options(arguments.parser)
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
@ -121,7 +121,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
logger.info('Filter, keep pairs %s' % pairs)
|
logger.info('Filter, keep pairs %s' % pairs)
|
||||||
|
|
||||||
tickers = optimize.load_data(
|
tickers = optimize.load_data(
|
||||||
datadir=args.datadir,
|
datadir=config.get('datadir'),
|
||||||
pairs=pairs,
|
pairs=pairs,
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=False,
|
refresh_pairs=False,
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import multiprocessing
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
PROC_COUNT = multiprocessing.cpu_count() - 1
|
|
||||||
DB_NAME = 'freqtrade_hyperopt'
|
|
||||||
WORK_DIR = os.path.join(
|
|
||||||
os.path.sep,
|
|
||||||
os.path.abspath(os.path.dirname(__file__)),
|
|
||||||
'..', '.hyperopt', 'worker'
|
|
||||||
)
|
|
||||||
if not os.path.exists(WORK_DIR):
|
|
||||||
os.makedirs(WORK_DIR)
|
|
||||||
|
|
||||||
# Spawn workers
|
|
||||||
command = [
|
|
||||||
'hyperopt-mongo-worker',
|
|
||||||
'--mongo=127.0.0.1:1234/{}'.format(DB_NAME),
|
|
||||||
'--poll-interval=0.1',
|
|
||||||
'--workdir={}'.format(WORK_DIR),
|
|
||||||
]
|
|
||||||
processes = [subprocess.Popen(command) for i in range(PROC_COUNT)]
|
|
||||||
|
|
||||||
# Join all workers
|
|
||||||
for proc in processes:
|
|
||||||
proc.wait()
|
|
@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
DB_PATH = os.path.join(
|
|
||||||
os.path.sep,
|
|
||||||
os.path.abspath(os.path.dirname(__file__)),
|
|
||||||
'..', '.hyperopt', 'mongodb'
|
|
||||||
)
|
|
||||||
if not os.path.exists(DB_PATH):
|
|
||||||
os.makedirs(DB_PATH)
|
|
||||||
|
|
||||||
subprocess.Popen([
|
|
||||||
'mongod',
|
|
||||||
'--bind_ip=127.0.0.1',
|
|
||||||
'--port=1234',
|
|
||||||
'--nohttpinterface',
|
|
||||||
'--dbpath={}'.format(DB_PATH),
|
|
||||||
]).wait()
|
|
4
setup.py
4
setup.py
@ -19,7 +19,7 @@ setup(name='freqtrade',
|
|||||||
packages=['freqtrade'],
|
packages=['freqtrade'],
|
||||||
scripts=['bin/freqtrade'],
|
scripts=['bin/freqtrade'],
|
||||||
setup_requires=['pytest-runner'],
|
setup_requires=['pytest-runner'],
|
||||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
tests_require=['pytest', 'pytest-mock', 'pytest-cov', 'moto'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ccxt',
|
'ccxt',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
@ -35,7 +35,7 @@ setup(name='freqtrade',
|
|||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'coinmarketcap',
|
'coinmarketcap'
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
"""
|
|
||||||
File that contains the configuration for Hyperopt
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def hyperopt_optimize_conf() -> dict:
|
|
||||||
"""
|
|
||||||
This function is used to define which parameters Hyperopt must used.
|
|
||||||
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
|
|
||||||
without MongoDB, Hyperopt will use the pair your have set in your config file.
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'max_open_trades': 3,
|
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'stake_amount': 0.01,
|
|
||||||
"minimal_roi": {
|
|
||||||
'40': 0.0,
|
|
||||||
'30': 0.01,
|
|
||||||
'20': 0.02,
|
|
||||||
'0': 0.04,
|
|
||||||
},
|
|
||||||
'stoploss': -0.10,
|
|
||||||
"bid_strategy": {
|
|
||||||
"ask_last_balance": 0.0
|
|
||||||
},
|
|
||||||
"exchange": {
|
|
||||||
"name": "bittrex",
|
|
||||||
"pair_whitelist": [
|
|
||||||
"ETH/BTC",
|
|
||||||
"LTC/BTC",
|
|
||||||
"ETC/BTC",
|
|
||||||
"DASH/BTC",
|
|
||||||
"ZEC/BTC",
|
|
||||||
"XLM/BTC",
|
|
||||||
"NXT/BTC",
|
|
||||||
"POWR/BTC",
|
|
||||||
"ADA/BTC",
|
|
||||||
"XMR/BTC"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
94
user_data/strategies/Long.py
Normal file
94
user_data/strategies/Long.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
# --- Do not remove these libs ---
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from typing import Dict, List
|
||||||
|
from hyperopt import hp
|
||||||
|
from functools import reduce
|
||||||
|
from pandas import DataFrame
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
import numpy # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class Long(IStrategy):
|
||||||
|
"""
|
||||||
|
|
||||||
|
author@: Gert Wohlgemuth
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
minimal_roi = {
|
||||||
|
"60": 0.05,
|
||||||
|
"30": 0.06,
|
||||||
|
"20": 0.07,
|
||||||
|
"0": 0.08
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
|
stoploss = -0.15
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = 60
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=50)
|
||||||
|
|
||||||
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
|
||||||
|
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||||
|
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||||
|
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||||
|
|
||||||
|
# SAR Parabol
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['macd'] > dataframe['macdsignal']) &
|
||||||
|
(dataframe['macd'] > 0) &
|
||||||
|
(dataframe['cci'] <= 0.0)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
# (dataframe['tema'] < dataframe['close'])
|
||||||
|
|
||||||
|
(dataframe['sar'] > dataframe['close']) &
|
||||||
|
(dataframe['fisher_rsi'] > 0.3)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
75
user_data/strategies/Quickie.py
Normal file
75
user_data/strategies/Quickie.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# --- Do not remove these libs ---
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from typing import Dict, List
|
||||||
|
from hyperopt import hp
|
||||||
|
from functools import reduce
|
||||||
|
from pandas import DataFrame
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
|
||||||
|
class Quickie(IStrategy):
|
||||||
|
"""
|
||||||
|
|
||||||
|
author@: Gert Wohlgemuth
|
||||||
|
|
||||||
|
idea:
|
||||||
|
momentum based strategie. The main idea is that it closes trades very quickly, while avoiding excessive losses. Hence a rather moderate stop loss in this case
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
minimal_roi = {
|
||||||
|
"60": 0.005,
|
||||||
|
"10": 0.01,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
|
stoploss = -0.25
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = 5
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
|
||||||
|
dataframe['sma_200'] = ta.SMA(dataframe, timeperiod=200)
|
||||||
|
dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50)
|
||||||
|
|
||||||
|
|
||||||
|
# required for graphing
|
||||||
|
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 30) &
|
||||||
|
(dataframe['tema'] < dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) &
|
||||||
|
(dataframe['sma_200'] > dataframe['close'])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
76
user_data/strategies/Simple.py
Normal file
76
user_data/strategies/Simple.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# --- Do not remove these libs ---
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from typing import Dict, List
|
||||||
|
from hyperopt import hp
|
||||||
|
from functools import reduce
|
||||||
|
from pandas import DataFrame
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
|
||||||
|
class Simple(IStrategy):
|
||||||
|
"""
|
||||||
|
|
||||||
|
author@: Gert Wohlgemuth
|
||||||
|
|
||||||
|
idea:
|
||||||
|
this strategy is based on the book, 'The Simple Strategy' and can be found in detail here:
|
||||||
|
|
||||||
|
https://www.amazon.com/Simple-Strategy-Powerful-Trading-Futures-ebook/dp/B00E66QPCG/ref=sr_1_1?ie=UTF8&qid=1525202675&sr=8-1&keywords=the+simple+strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# since this strategy is planned around 5 minutes, we assume any time we have a 5% profit we should call it a day
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.01
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
|
stoploss = -0.25
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = 5
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
# MACD
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)
|
||||||
|
|
||||||
|
# required for graphing
|
||||||
|
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=12, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(dataframe['macd'] > 0) # over 0
|
||||||
|
& (dataframe['macd'] > dataframe['macdsignal']) # over signal
|
||||||
|
& (dataframe['bb_upperband'] > dataframe['bb_upperband'].shift(1)) # pointed up
|
||||||
|
& (dataframe['rsi'] > 70) # optional filter, need to investigate
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
# different strategy used for sell points, due to be able to duplicate it to 100%
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['rsi'] > 80)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
90
user_data/strategies/ZLC.py
Normal file
90
user_data/strategies/ZLC.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# --- Do not remove these libs ---
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from typing import Dict, List
|
||||||
|
from hyperopt import hp
|
||||||
|
from functools import reduce
|
||||||
|
from pandas import DataFrame
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
|
||||||
|
|
||||||
|
class ZLC(IStrategy):
|
||||||
|
"""
|
||||||
|
|
||||||
|
author@: Gert Wohlgemuth
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
minimal_roi = {
|
||||||
|
"60": 0.01,
|
||||||
|
"30": 0.03,
|
||||||
|
"20": 0.04,
|
||||||
|
"0": 0.01
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
|
stoploss = -0.3
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = 5
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
dataframe['cci-slow'] = ta.CCI(dataframe, timeperiod=25)
|
||||||
|
dataframe['cci-fast'] = ta.CCI(dataframe, timeperiod=50)
|
||||||
|
dataframe['expo'] = ta.EMA(dataframe, timeperiod=35)
|
||||||
|
|
||||||
|
# required for graphing
|
||||||
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
|
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
#don't buy on peak tops
|
||||||
|
(dataframe['close'] < dataframe['bb_middleband'])
|
||||||
|
# this is the main concept of evaluating buys
|
||||||
|
& (dataframe['cci-fast'] > 0)
|
||||||
|
& (dataframe['cci-slow'] > 0)
|
||||||
|
& (dataframe['close'] > dataframe['expo'])
|
||||||
|
|
||||||
|
)
|
||||||
|
,
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe['close'] >= dataframe['bb_upperband']) |
|
||||||
|
(
|
||||||
|
(dataframe['cci-fast'] < 0)
|
||||||
|
& (dataframe['cci-slow'] < 0)
|
||||||
|
& (dataframe['close'] < dataframe['expo'])
|
||||||
|
|
||||||
|
)
|
||||||
|
,
|
||||||
|
'sell'] = 0
|
||||||
|
return dataframe
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# --- Do not remove these libs ---
|
# --- Do not remove these libs ---
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -7,7 +6,7 @@ from pandas import DataFrame
|
|||||||
# Add your lib to import here
|
# Add your lib to import here
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
import numpy # noqa
|
import numpy # noqa
|
||||||
|
|
||||||
|
|
||||||
# This class is a sample. Feel free to customize it.
|
# This class is a sample. Feel free to customize it.
|
||||||
@ -218,9 +217,9 @@ class TestStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 30) &
|
(dataframe['adx'] > 30) &
|
||||||
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
|
|
||||||
@ -234,9 +233,9 @@ class TestStrategy(IStrategy):
|
|||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 70) &
|
(dataframe['adx'] > 70) &
|
||||||
(dataframe['tema'] > dataframe['bb_middleband']) &
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
|
Loading…
Reference in New Issue
Block a user