Merge pull request #2 from nullart/nullartHFT

Nullart HFT
This commit is contained in:
nullart 2018-06-27 18:57:22 +08:00 committed by GitHub
commit 1aa48f5f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 2572 additions and 1334 deletions

View File

@ -2,4 +2,5 @@
omit = omit =
scripts/* scripts/*
freqtrade/tests/* freqtrade/tests/*
freqtrade/vendor/* freqtrade/vendor/*
freqtrade/__main__.py

4
.flake8 Normal file
View File

@ -0,0 +1,4 @@
[flake8]
ignore = E226,E302,E41,E126,F841
max-line-length = 160
exclude = */tests/*

View File

@ -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
View File

@ -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/

View File

@ -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)

View File

@ -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 && \

View File

@ -1,10 +1,20 @@
# freqtrade # freqtrade
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade) [![Build Status](https://travis-ci.org/berlinguyinca/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/berlinguyinca/freqtrade/badge.svg?branch=wohlgemuth)](https://coveralls.io/github/berlinguyinca/freqtrade?branch=wohlgemuth)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/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

View File

@ -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,

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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
View 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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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})

View File

@ -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': {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'):

View File

@ -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

View File

@ -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]:
""" """

View File

@ -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'] = ''

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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'],

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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(

View File

@ -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']

View File

@ -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=[],

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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']

View File

@ -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,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

Binary file not shown.

35
lib/libta_lib.la Executable file
View 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
View File

@ -0,0 +1 @@
libta_lib.so.0.0.0

BIN
lib/libta_lib.so.0.0.0 Executable file

Binary file not shown.

View File

@ -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

View File

@ -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.")

View File

@ -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)

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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,

View File

@ -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"
]
}
}

View 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

View 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

View 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

View 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

View File

@ -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