merge develop to master for 0.16.1 release (pre-work for ccxt into use)
This commit is contained in:
commit
4ce927d455
@ -4,3 +4,12 @@ Dockerfile
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
config.json*
|
config.json*
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
.coveragerc
|
||||||
|
.eggs
|
||||||
|
.github
|
||||||
|
.pylintrc
|
||||||
|
.travis.yml
|
||||||
|
CONTRIBUTING.md
|
||||||
|
MANIFEST.in
|
||||||
|
README.md
|
||||||
|
freqtrade.service
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -5,6 +5,9 @@ config.json
|
|||||||
*.sqlite
|
*.sqlite
|
||||||
.hyperopt
|
.hyperopt
|
||||||
logfile.txt
|
logfile.txt
|
||||||
|
hyperopt_trials.pickle
|
||||||
|
user_data/
|
||||||
|
freqtrade-plot.html
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@ -86,4 +89,4 @@ target/
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
hyperopt_trials.pickle
|
.pytest_cache/
|
||||||
|
@ -13,7 +13,7 @@ addons:
|
|||||||
install:
|
install:
|
||||||
- ./install_ta-lib.sh
|
- ./install_ta-lib.sh
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install --upgrade flake8 coveralls
|
- pip install --upgrade flake8 coveralls pytest-random-order
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
FROM python:3.6.2
|
FROM python:3.6.5-slim-stretch
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
RUN apt-get update && apt-get -y install build-essential && apt-get clean
|
RUN apt-get update && apt-get -y install curl build-essential && 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 && \
|
||||||
|
25
README.md
25
README.md
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
|
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
||||||
|
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/gcarq/freqtrade/maintainability)
|
||||||
|
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies designed to
|
Simple High frequency trading bot for crypto currencies designed to
|
||||||
@ -80,6 +81,13 @@ bot in dry-run. We invite you to read the
|
|||||||
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
||||||
to ensure you understand how the bot is working.
|
to ensure you understand how the bot is working.
|
||||||
|
|
||||||
|
### Easy installation
|
||||||
|
The script below will install all dependencies and help you to configure the bot.
|
||||||
|
```bash
|
||||||
|
./setup.sh --install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual installation
|
||||||
The following steps are made for Linux/MacOS environment
|
The following steps are made for Linux/MacOS environment
|
||||||
|
|
||||||
**1. Clone the repo**
|
**1. Clone the repo**
|
||||||
@ -97,7 +105,7 @@ vi config.json
|
|||||||
**3. Build your docker image and run it**
|
**3. Build your docker image and run it**
|
||||||
```bash
|
```bash
|
||||||
docker build -t freqtrade .
|
docker build -t freqtrade .
|
||||||
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -136,8 +144,8 @@ to understand the requirements before sending your pull-requests.
|
|||||||
### Bot commands
|
### Bot commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
|
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
|
||||||
[--dry-run-db]
|
[--dynamic-whitelist [INT]]
|
||||||
{backtesting,hyperopt} ...
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
@ -149,16 +157,17 @@ positional arguments:
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-c PATH, --config PATH
|
|
||||||
specify configuration file (default: config.json)
|
|
||||||
-v, --verbose be verbose
|
-v, --verbose be verbose
|
||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
--dynamic-whitelist [INT]
|
-c PATH, --config PATH
|
||||||
dynamically generate and update whitelist based on 24h
|
specify configuration file (default: config.json)
|
||||||
BaseVolume (Default 20 currencies)
|
|
||||||
--dry-run-db Force dry run to use a local DB
|
--dry-run-db Force dry run to use a local DB
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||||
only if dry_run is enabled.
|
only if dry_run is enabled.
|
||||||
|
--datadir PATH path to backtest data (default freqdata/tests/testdata
|
||||||
|
--dynamic-whitelist [INT]
|
||||||
|
dynamically generate and update whitelist based on 24h
|
||||||
|
BaseVolume (Default 20 currencies)
|
||||||
```
|
```
|
||||||
More details on:
|
More details on:
|
||||||
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from freqtrade.main import main
|
import sys
|
||||||
main()
|
|
||||||
|
from freqtrade.main import main, set_loggers
|
||||||
|
set_loggers()
|
||||||
|
main(sys.argv[1:])
|
||||||
|
@ -4,21 +4,14 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"minimal_roi": {
|
|
||||||
"40": 0.0,
|
|
||||||
"30": 0.01,
|
|
||||||
"20": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
},
|
|
||||||
"stoploss": -0.10,
|
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": 600,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "key",
|
"key": "your_exchange_key",
|
||||||
"secret": "secret",
|
"secret": "your_exchange_secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC_ETH",
|
"BTC_ETH",
|
||||||
"BTC_LTC",
|
"BTC_LTC",
|
||||||
@ -41,8 +34,8 @@
|
|||||||
},
|
},
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"token": "token",
|
"token": "your_telegram_token",
|
||||||
"chat_id": "chat_id"
|
"chat_id": "your_telegram_chat_id"
|
||||||
},
|
},
|
||||||
"initial_state": "running",
|
"initial_state": "running",
|
||||||
"internals": {
|
"internals": {
|
||||||
|
54
config_full.json.example
Normal file
54
config_full.json.example
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"max_open_trades": 3,
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.05,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
|
"dry_run": false,
|
||||||
|
"ticker_interval": 5,
|
||||||
|
"minimal_roi": {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
},
|
||||||
|
"stoploss": -0.10,
|
||||||
|
"unfilledtimeout": 600,
|
||||||
|
"bid_strategy": {
|
||||||
|
"ask_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
|
"key": "your_exchange_key",
|
||||||
|
"secret": "your_exchange_secret",
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC_ETH",
|
||||||
|
"BTC_LTC",
|
||||||
|
"BTC_ETC",
|
||||||
|
"BTC_DASH",
|
||||||
|
"BTC_ZEC",
|
||||||
|
"BTC_XLM",
|
||||||
|
"BTC_NXT",
|
||||||
|
"BTC_POWR",
|
||||||
|
"BTC_ADA",
|
||||||
|
"BTC_XMR"
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"BTC_DOGE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"use_sell_signal": false,
|
||||||
|
"sell_profit_only": false
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "your_telegram_token",
|
||||||
|
"chat_id": "your_telegram_chat_id"
|
||||||
|
},
|
||||||
|
"initial_state": "running",
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
},
|
||||||
|
"strategy": "DefaultStrategy",
|
||||||
|
"strategy_path": "/some/folder/"
|
||||||
|
}
|
@ -51,6 +51,61 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
|||||||
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**
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py -s currentstrategy backtesting
|
||||||
|
```
|
||||||
|
Where `-s currentstrategy` refers to a filename `currentstrategy.py` in `freqtrade/user_data/strategies`
|
||||||
|
|
||||||
|
**Exporting trades to file**
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
|
```
|
||||||
|
|
||||||
|
**Running backtest with smaller testset**
|
||||||
|
Use the `--timerange` argument to change how much of the testset
|
||||||
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py backtesting --timerange=-200
|
||||||
|
```
|
||||||
|
|
||||||
|
***Advanced use of timerange***
|
||||||
|
Doing `--timerange=-200` will get the last 200 timeframes
|
||||||
|
from your inputdata. You can also specify specific dates,
|
||||||
|
or a range span indexed by start and stop.
|
||||||
|
|
||||||
|
The full timerange specification:
|
||||||
|
- Use last 123 tickframes of data: `--timerange=-123`
|
||||||
|
- Use first 123 tickframes of data: `--timerange=123-`
|
||||||
|
- Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||||
|
|
||||||
|
|
||||||
|
Incoming feature, not implemented yet:
|
||||||
|
- `--timerange=-20180131`
|
||||||
|
- `--timerange=20180101-`
|
||||||
|
- `--timerange=20180101-20181231`
|
||||||
|
|
||||||
|
|
||||||
|
**Update testdata directory**
|
||||||
|
To update your testdata directory, or download into another testdata directory:
|
||||||
|
```bash
|
||||||
|
mkdir -p user_data/data/testdata-20180113
|
||||||
|
cp freqtrade/tests/testdata/pairs.json user_data/data-20180113
|
||||||
|
cd user_data/data-20180113
|
||||||
|
```
|
||||||
|
|
||||||
|
Possibly edit pairs.json file to include/exclude pairs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 freqtrade/tests/testdata/download_backtest_data.py -p pairs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will read your pairs.json file, and download ticker data
|
||||||
|
into the current working directory.
|
||||||
|
|
||||||
|
|
||||||
For help about backtesting usage, please refer to
|
For help about backtesting usage, please refer to
|
||||||
[Backtesting commands](#backtesting-commands).
|
[Backtesting commands](#backtesting-commands).
|
||||||
|
|
||||||
|
@ -3,21 +3,62 @@ This page explains where to customize your strategies, and add new
|
|||||||
indicators.
|
indicators.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Change your strategy](#change-your-strategy)
|
- [Install a custom strategy file](#install-a-custom-strategy-file)
|
||||||
|
- [Customize your strategy](#change-your-strategy)
|
||||||
- [Add more Indicator](#add-more-indicator)
|
- [Add more Indicator](#add-more-indicator)
|
||||||
|
- [Where is the default strategy](#where-is-the-default-strategy)
|
||||||
|
|
||||||
|
Since the version `0.16.0` the bot allows using custom strategy file.
|
||||||
|
|
||||||
|
## Install a custom strategy file
|
||||||
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
|
`user_data/strategies`.
|
||||||
|
|
||||||
|
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
|
||||||
|
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
|
||||||
|
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
|
```
|
||||||
|
|
||||||
## Change your strategy
|
## Change your strategy
|
||||||
The bot is using buy and sell strategies to buy and sell your trades.
|
The bot includes a default strategy file. However, we recommend you to
|
||||||
Both are customizable.
|
use your own file to not have to lose your parameters every time the default
|
||||||
|
strategy file will be updated on Github. Put your custom strategy file
|
||||||
|
into the folder `user_data/strategies`.
|
||||||
|
|
||||||
|
A strategy file contains all the information needed to build a good strategy:
|
||||||
|
- Buy strategy rules
|
||||||
|
- Sell strategy rules
|
||||||
|
- Minimal ROI recommended
|
||||||
|
- Stoploss recommended
|
||||||
|
- Hyperopt parameter
|
||||||
|
|
||||||
|
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
||||||
|
You can test it with the parameter: `--strategy TestStrategy`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specify custom strategy location
|
||||||
|
If you want to use a strategy from a different folder you can pass `--strategy-path`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
|
file as reference.**
|
||||||
|
|
||||||
### Buy strategy
|
### Buy strategy
|
||||||
The default buy strategy is located in the file
|
Edit the method `populate_buy_trend()` into your strategy file to
|
||||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73-L92).
|
update your buy strategy.
|
||||||
Edit the function `populate_buy_trend()` to update your buy strategy.
|
|
||||||
|
|
||||||
Sample:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(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
|
||||||
@ -25,14 +66,9 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['rsi'] < 35) &
|
|
||||||
(dataframe['fastd'] < 35) &
|
|
||||||
(dataframe['adx'] > 30) &
|
(dataframe['adx'] > 30) &
|
||||||
(dataframe['plus_di'] > 0.5)
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
) |
|
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||||
(
|
|
||||||
(dataframe['adx'] > 65) &
|
|
||||||
(dataframe['plus_di'] > 0.5)
|
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
|
|
||||||
@ -40,41 +76,31 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Sell strategy
|
### Sell strategy
|
||||||
The default buy strategy is located in the file
|
Edit the method `populate_sell_trend()` into your strategy file to
|
||||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115)
|
update your sell strategy.
|
||||||
Edit the function `populate_sell_trend()` to update your buy strategy.
|
|
||||||
|
|
||||||
Sample:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
```python
|
```python
|
||||||
def populate_sell_trend(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 buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
|
||||||
(
|
|
||||||
(crossed_above(dataframe['rsi'], 70)) |
|
|
||||||
(crossed_above(dataframe['fastd'], 70))
|
|
||||||
) &
|
|
||||||
(dataframe['adx'] > 10) &
|
|
||||||
(dataframe['minus_di'] > 0)
|
|
||||||
) |
|
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 70) &
|
(dataframe['adx'] > 70) &
|
||||||
(dataframe['minus_di'] > 0.5)
|
(dataframe['tema'] > dataframe['blower']) &
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Add more Indicator
|
## Add more Indicator
|
||||||
As you have seen, buy and sell strategies need indicators. You can see
|
As you have seen, buy and sell strategies need indicators. You can add
|
||||||
the indicators in the file
|
more indicators by extending the list contained in
|
||||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115).
|
the method `populate_indicators()` from your strategy file.
|
||||||
Of course you can add more indicators by extending the list contained in
|
|
||||||
the function `populate_indicators()`.
|
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
```python
|
```python
|
||||||
@ -111,6 +137,15 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Want more indicators example?**
|
||||||
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||||
|
Then uncomment indicators you need.
|
||||||
|
|
||||||
|
|
||||||
|
### Where is the default strategy?
|
||||||
|
The default buy strategy is located in the file
|
||||||
|
[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
|
||||||
|
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have a perfect strategy you probably want to backtesting it.
|
Now you have a perfect strategy you probably want to backtesting it.
|
||||||
|
@ -22,19 +22,21 @@ positional arguments:
|
|||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-c PATH, --config PATH
|
|
||||||
specify configuration file (default: config.json)
|
|
||||||
-v, --verbose be verbose
|
-v, --verbose be verbose
|
||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-dd PATH, --datadir PATH
|
-c PATH, --config PATH
|
||||||
Path is from where backtesting and hyperopt will load the
|
specify configuration file (default: config.json)
|
||||||
ticker data files (default freqdata/tests/testdata).
|
-s NAME, --strategy NAME
|
||||||
--dynamic-whitelist [INT]
|
specify strategy class name (default: DefaultStrategy)
|
||||||
dynamically generate and update whitelist based on 24h
|
--strategy-path PATH specify additional strategy lookup path
|
||||||
BaseVolume (Default 20 currencies)
|
|
||||||
--dry-run-db Force dry run to use a local DB
|
--dry-run-db Force dry run to use a local DB
|
||||||
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
"tradesv3.dry_run.sqlite" instead of memory DB. Work
|
||||||
only if dry_run is enabled.
|
only if dry_run is enabled.
|
||||||
|
--datadir PATH
|
||||||
|
path to backtest data (default freqdata/tests/testdata
|
||||||
|
--dynamic-whitelist [INT]
|
||||||
|
dynamically generate and update whitelist based on 24h
|
||||||
|
BaseVolume (Default 20 currencies)
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different config file?
|
### How to use a different config file?
|
||||||
@ -45,6 +47,38 @@ default, the bot will load the file `./config.json`
|
|||||||
python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### How to use --strategy?
|
||||||
|
This parameter will allow you to load your custom strategy class.
|
||||||
|
Per default without `--strategy` or `-s` the bot will load the
|
||||||
|
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
|
||||||
|
|
||||||
|
The bot will search your strategy file within `user_data/strategies` and `freqtrade/strategy`.
|
||||||
|
|
||||||
|
To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
|
||||||
|
a strategy class called `AwesomeStrategy` to load it:
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
|
```
|
||||||
|
|
||||||
|
If the bot does not find your strategy file, it will display in an error
|
||||||
|
message the reason (File not found, or errors in your code).
|
||||||
|
|
||||||
|
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
|
### How to use --strategy-path?
|
||||||
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
|
checked before the default locations (The passed path must be a folder!):
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
|
||||||
|
```
|
||||||
|
|
||||||
|
#### How to install a strategy?
|
||||||
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
|
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
||||||
|
|
||||||
### How to use --dynamic-whitelist?
|
### How to use --dynamic-whitelist?
|
||||||
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||||
on BaseVolume. This value can be changed when you run the script.
|
on BaseVolume. This value can be changed when you run the script.
|
||||||
|
@ -17,10 +17,11 @@ The table below will list all configuration parameters.
|
|||||||
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
|
||||||
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
|
||||||
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
|
||||||
|
| `ticker_interval` | [1, 5, 30, 60, 1440] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes
|
||||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||||
| `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below.
|
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below.
|
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
|
||||||
@ -29,10 +30,13 @@ The table below will list all configuration parameters.
|
|||||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||||
|
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||||
| `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 is `enable` 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 is `enable` is `true`.
|
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||||
|
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
||||||
|
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||||
|
|
||||||
The definition of each config parameters is in
|
The definition of each config parameters is in
|
||||||
@ -51,11 +55,19 @@ See the example below:
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
### Understand stoploss
|
### Understand 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`
|
||||||
|
value. This parameter is optional. If you use it, it will take over the
|
||||||
|
`stoploss` value from the strategy file.
|
||||||
|
|
||||||
### Understand initial_state
|
### Understand 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`)
|
Possible values are `running` or `stopped`. (default=`running`)
|
||||||
|
62
docs/faq.md
62
docs/faq.md
@ -2,20 +2,70 @@
|
|||||||
|
|
||||||
#### I have waited 5 minutes, why hasn't the bot made any trades yet?!
|
#### I have waited 5 minutes, why hasn't the bot made any trades yet?!
|
||||||
|
|
||||||
Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient!
|
Depending on the buy strategy, the amount of whitelisted coins, the
|
||||||
|
situation of the market etc, it can take up to hours to find good entry
|
||||||
|
position for a trade. Be patient!
|
||||||
|
|
||||||
#### I have made 12 trades already, why is my total profit negative?!
|
#### I have made 12 trades already, why is my total profit negative?!
|
||||||
|
|
||||||
I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our current algorithm does leave you on the plus side, but that is after thousands of trades and even there, you will be left with losses on specific coins that you have traded tens if not hundreds of times. We of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades.
|
I understand your disappointment but unfortunately 12 trades is just
|
||||||
|
not enough to say anything. If you run backtesting, you can see that our
|
||||||
|
current algorithm does leave you on the plus side, but that is after
|
||||||
|
thousands of trades and even there, you will be left with losses on
|
||||||
|
specific coins that you have traded tens if not hundreds of times. We
|
||||||
|
of course constantly aim to improve the bot but it will _always_ be a
|
||||||
|
gamble, which should leave you with modest wins on monthly basis but
|
||||||
|
you can't say much from few trades.
|
||||||
|
|
||||||
#### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
|
#### I’d like to change the stake amount. Can I just stop the bot with
|
||||||
|
/stop and then change the config.json and run it again?
|
||||||
|
|
||||||
Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again.
|
Not quite. Trades are persisted to a database but the configuration is
|
||||||
|
currently only read when the bot is killed and restarted. `/stop` more
|
||||||
|
like pauses. You can stop your bot, adjust settings and start it again.
|
||||||
|
|
||||||
#### I want to improve the bot with a new strategy
|
#### I want to improve the bot with a new strategy
|
||||||
|
|
||||||
That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [[here|Testing-new-strategies-with-Hyperopt]].
|
That's great. We have a nice backtesting and hyperoptimizing setup. See
|
||||||
|
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
|
||||||
|
|
||||||
#### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
|
#### Is there a setting to only SELL the coins being held and not
|
||||||
|
perform anymore BUYS?
|
||||||
|
|
||||||
You can use the `/forcesell all` command from Telegram.
|
You can use the `/forcesell all` command from Telegram.
|
||||||
|
|
||||||
|
### How many epoch do I need to get a good Hyperopt result?
|
||||||
|
Per default Hyperopts without `-e` or `--epochs` parameter will only
|
||||||
|
run 100 epochs, means 100 evals of your triggers, guards, .... Too few
|
||||||
|
to find a great result (unless if you are very lucky), so you probably
|
||||||
|
have to run it for 10.000 or more. But it will take an eternity to
|
||||||
|
compute.
|
||||||
|
|
||||||
|
We recommend you to run it at least 10.000 epochs:
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py hyperopt -e 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
or if you want intermediate result to see
|
||||||
|
```bash
|
||||||
|
for i in {1..100}; do python3 ./freqtrade/main.py hyperopt -e 100; done
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Why it is so long to run hyperopt?
|
||||||
|
Finding a great Hyperopt results takes time.
|
||||||
|
|
||||||
|
If you wonder why it takes a while to find great hyperopt results
|
||||||
|
|
||||||
|
This answer was written during the under the release 0.15.1, when we had
|
||||||
|
:
|
||||||
|
- 8 triggers
|
||||||
|
- 9 guards: let's say we evaluate even 10 values from each
|
||||||
|
- 1 stoploss calculation: let's say we want 10 values from that too to
|
||||||
|
be evaluated
|
||||||
|
|
||||||
|
The following calculation is still very rough and not very precise
|
||||||
|
but it will give the idea. With only these triggers and guards there is
|
||||||
|
already 8*10^9*10 evaluations. A roughly total of 80 billion evals.
|
||||||
|
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
|
||||||
|
of the search space.
|
||||||
|
|
||||||
|
@ -14,14 +14,13 @@ parameters with Hyperopt.
|
|||||||
|
|
||||||
## Prepare Hyperopt
|
## Prepare Hyperopt
|
||||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
Before we start digging in Hyperopt, we recommend you to take a look at
|
||||||
out Hyperopt file
|
your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
||||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
|
||||||
|
|
||||||
### 1. Configure your Guards and Triggers
|
### 1. Configure your Guards and Triggers
|
||||||
There are two places you need to change to add a new buy strategy for
|
There are two places you need to change in your strategy file to add a
|
||||||
testing:
|
new buy strategy for testing:
|
||||||
- Inside the [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L167-L207).
|
- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
||||||
- Inside the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
|
- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
||||||
|
|
||||||
There you have two different type of indicators: 1. `guards` and 2.
|
There you have two different type of indicators: 1. `guards` and 2.
|
||||||
`triggers`.
|
`triggers`.
|
||||||
@ -38,10 +37,10 @@ ADX > 10*".
|
|||||||
|
|
||||||
|
|
||||||
If you have updated the buy strategy, means change the content of
|
If you have updated the buy strategy, means change the content of
|
||||||
`populate_buy_trend()` function you have to update the `guards` and
|
`populate_buy_trend()` method you have to update the `guards` and
|
||||||
`triggers` hyperopts must used.
|
`triggers` hyperopts must used.
|
||||||
|
|
||||||
As for an example if your `populate_buy_trend()` function is:
|
As for an example if your `populate_buy_trend()` method is:
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
@ -52,14 +51,14 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
Your hyperopt file must contains `guards` to find the right value for
|
Your hyperopt file must contain `guards` to find the right value for
|
||||||
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
||||||
means you will need to enable/disable triggers.
|
means you will need to enable/disable triggers.
|
||||||
|
|
||||||
In our case the `SPACE` and `populate_buy_trend` in hyperopt.py file
|
In our case the `SPACE` and `populate_buy_trend` in your strategy file
|
||||||
will be look like:
|
will look like:
|
||||||
```python
|
```python
|
||||||
SPACE = {
|
space = {
|
||||||
'rsi': hp.choice('rsi', [
|
'rsi': hp.choice('rsi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||||
@ -82,7 +81,7 @@ SPACE = {
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if params['adx']['enabled']:
|
if params['adx']['enabled']:
|
||||||
@ -106,18 +105,18 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
|
|
||||||
|
|
||||||
### 2. Update the hyperopt config file
|
### 2. Update the hyperopt config file
|
||||||
Hyperopt is using a dedicated config file. At this moment hyperopt
|
Hyperopt is using a dedicated config file. Currently hyperopt
|
||||||
cannot use your config file. It is also made on purpose to allow you
|
cannot use your config file. It is also made on purpose to allow you
|
||||||
testing your strategy with different configurations.
|
testing your strategy with different configurations.
|
||||||
|
|
||||||
The Hyperopt configuration is located in
|
The Hyperopt configuration is located in
|
||||||
[freqtrade/optimize/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt_conf.py).
|
[user_data/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
||||||
|
|
||||||
|
|
||||||
## Advanced notions
|
## Advanced notions
|
||||||
### Understand the Guards and Triggers
|
### Understand the Guards and Triggers
|
||||||
When you need to add the new guards and triggers to be hyperopt
|
When you need to add the new guards and triggers to be hyperopt
|
||||||
parameters, you do this by adding them into the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
|
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
||||||
|
|
||||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
||||||
|
|
||||||
@ -128,19 +127,21 @@ If it's a guard, you will add a line like this:
|
|||||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
||||||
]),
|
]),
|
||||||
```
|
```
|
||||||
This says, "*one of guards is RSI, it can have two values, enabled or
|
This says, "*one of the guards is RSI, it can have two values, enabled or
|
||||||
disabled. If it is enabled, try different values for it between 20 and 40*".
|
disabled. If it is enabled, try different values for it between 20 and 40*".
|
||||||
|
|
||||||
So, the part of the strategy builder using the above setting looks like
|
So, the part of the strategy builder using the above setting looks like
|
||||||
this:
|
this:
|
||||||
|
|
||||||
```
|
```
|
||||||
if params['rsi']['enabled']:
|
if params['rsi']['enabled']:
|
||||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||||
```
|
```
|
||||||
|
|
||||||
It checks if Hyperopt wants the RSI guard to be enabled for this
|
It checks if Hyperopt wants the RSI guard to be enabled for this
|
||||||
round `params['rsi']['enabled']` and if it is, then it will add a
|
round `params['rsi']['enabled']` and if it is, then it will add a
|
||||||
condition that says RSI must be < than the value hyperopt picked
|
condition that says RSI must be smaller than the value hyperopt picked
|
||||||
for this evaluation, that is given in the `params['rsi']['value']`.
|
for this evaluation, which is given in the `params['rsi']['value']`.
|
||||||
|
|
||||||
That's it. Now you can add new parts of strategies to Hyperopt and it
|
That's it. Now you can add new parts of strategies to Hyperopt and it
|
||||||
will try all the combinations with all different values in the search
|
will try all the combinations with all different values in the search
|
||||||
@ -149,9 +150,7 @@ for best working algo.
|
|||||||
|
|
||||||
### Add a new Indicators
|
### Add a new Indicators
|
||||||
If you want to test an indicator that isn't used by the bot currently,
|
If you want to test an indicator that isn't used by the bot currently,
|
||||||
you need to add it to
|
you need to add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||||
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L40-L70)
|
|
||||||
inside the `populate_indicators` function.
|
|
||||||
|
|
||||||
## Execute Hyperopt
|
## Execute Hyperopt
|
||||||
Once you have updated your hyperopt configuration you can run it.
|
Once you have updated your hyperopt configuration you can run it.
|
||||||
@ -160,13 +159,40 @@ it will take time you will have the result (more than 30 mins).
|
|||||||
|
|
||||||
We strongly recommend to use `screen` to prevent any connection loss.
|
We strongly recommend to use `screen` to prevent any connection loss.
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py -c config.json hyperopt
|
python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||||
|
running at least several thousand evaluations.
|
||||||
|
|
||||||
### Execute hyperopt with different ticker-data source
|
### Execute hyperopt with different ticker-data source
|
||||||
If you would like to learn parameters using an alternate ticke-data that
|
If you would like to hyperopt parameters using an alternate ticker data that
|
||||||
you have on-disk, use the --datadir PATH option. Default hyperopt will
|
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||||
use data from directory freqtrade/tests/testdata.
|
use data from directory `user_data/data`.
|
||||||
|
|
||||||
|
### Running hyperopt with smaller testset
|
||||||
|
Use the `--timeperiod` argument to change how much of the testset
|
||||||
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running hyperopt with smaller search space
|
||||||
|
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||||
|
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||||
|
might make more sense to start by just searching for initial buy algorithm.
|
||||||
|
Or maybe you just want to optimize your stoploss or roi table for that awesome
|
||||||
|
new buy strategy you have.
|
||||||
|
|
||||||
|
Legal values are:
|
||||||
|
|
||||||
|
- `all`: optimize everything
|
||||||
|
- `buy`: just search for a new buy strategy
|
||||||
|
- `roi`: just optimize the minimal profit table for your strategy
|
||||||
|
- `stoploss`: search for the best stoploss value
|
||||||
|
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
|
||||||
### Hyperopt with MongoDB
|
### Hyperopt with MongoDB
|
||||||
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
|
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
|
||||||
@ -259,24 +285,19 @@ customizable value.
|
|||||||
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
|
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
|
||||||
- and so on...
|
- and so on...
|
||||||
|
|
||||||
|
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||||
You have to look from
|
method, what those values match to.
|
||||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200)
|
|
||||||
what those values match to.
|
|
||||||
|
|
||||||
So for example you had `adx:` with the `value: 15.0` so we would look
|
So for example you had `adx:` with the `value: 15.0` so we would look
|
||||||
at `adx`-block from
|
at `adx`-block, that translates to the following code block:
|
||||||
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179).
|
|
||||||
That translates to the following code block to
|
|
||||||
[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73)
|
|
||||||
```
|
```
|
||||||
(dataframe['adx'] > 15.0)
|
(dataframe['adx'] > 15.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
So translating your whole hyperopt result to as the new buy-signal
|
Translating your whole hyperopt result to as the new buy-signal
|
||||||
would be the following:
|
would be the following:
|
||||||
```
|
```
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 15.0) & # adx-value
|
(dataframe['adx'] > 15.0) & # adx-value
|
||||||
|
@ -1,111 +1,167 @@
|
|||||||
# Install the bot
|
# Installation
|
||||||
|
|
||||||
This page explains how to prepare your environment for running the bot.
|
This page explains how to prepare your environment for running the bot.
|
||||||
To understand how to set up the bot please read the Bot
|
|
||||||
[Bot configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
To understand how to set up the bot please read the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
page.
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Docker Automatic Installation](#docker)
|
|
||||||
- [Linux or Mac manual Installation](#linux--mac)
|
|
||||||
- [Linux - Ubuntu 16.04](#21-linux---ubuntu-1604)
|
|
||||||
- [Linux - Other distro](#22-linux---other-distro)
|
|
||||||
- [MacOS installation](#23-macos-installation)
|
|
||||||
- [Advanced Linux ](#advanced-linux)
|
|
||||||
- [Windows manual Installation](#windows)
|
|
||||||
|
|
||||||
# Docker
|
* [Table of Contents](#table-of-contents)
|
||||||
|
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
||||||
|
* [Automatic Installation - Docker](#automatic-installation---docker)
|
||||||
|
* [Custom Linux MacOS Installation](#custom-installation)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [Linux - Ubuntu 16.04](#linux---ubuntu-1604)
|
||||||
|
- [MacOS](#macos)
|
||||||
|
- [Setup Config and virtual env](#setup-config-and-virtual-env)
|
||||||
|
* [Windows](#windows)
|
||||||
|
|
||||||
|
|
||||||
|
<!-- /TOC -->
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## Easy Installation - Linux Script
|
||||||
|
|
||||||
|
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./setup.sh
|
||||||
|
usage:
|
||||||
|
-i,--install Install freqtrade from scratch
|
||||||
|
-u,--update Command git pull to update.
|
||||||
|
-r,--reset Hard reset your develop/master branch.
|
||||||
|
-c,--config Easy config generator (Will override your existing file).
|
||||||
|
```
|
||||||
|
|
||||||
|
### --install
|
||||||
|
This script will install everything you need to run the bot:
|
||||||
|
* Mandatory software as: `Python3`, `ta-lib`, `wget`
|
||||||
|
* Setup your virtualenv
|
||||||
|
* Configure your `config.json` file
|
||||||
|
|
||||||
|
This script is a combination of `install script` `--reset`, `--config`
|
||||||
|
|
||||||
|
### --update
|
||||||
|
Update parameter will pull the last version of your current branch and update your virtualenv.
|
||||||
|
|
||||||
|
### --reset
|
||||||
|
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
|
||||||
|
|
||||||
|
### --config
|
||||||
|
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## Automatic Installation - Docker
|
||||||
|
|
||||||
## Easy installation
|
|
||||||
Start by downloading Docker for your platform:
|
Start by downloading Docker for your platform:
|
||||||
- [Mac](https://www.docker.com/products/docker#/mac)
|
|
||||||
- [Windows](https://www.docker.com/products/docker#/windows)
|
|
||||||
- [Linux](https://www.docker.com/products/docker#/linux)
|
|
||||||
|
|
||||||
Once you have Docker installed, simply create the config file
|
* [Mac](https://www.docker.com/products/docker#/mac)
|
||||||
(e.g. `config.json`) and then create a Docker image for `freqtrade`
|
* [Windows](https://www.docker.com/products/docker#/windows)
|
||||||
using the Dockerfile in this repo.
|
* [Linux](https://www.docker.com/products/docker#/linux)
|
||||||
|
|
||||||
|
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
|
||||||
|
|
||||||
|
|
||||||
|
### 1. Prepare the Bot
|
||||||
|
|
||||||
|
#### 1.1. Clone the git repository
|
||||||
|
|
||||||
### 1. Prepare the bot
|
|
||||||
1. Clone the git
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/gcarq/freqtrade.git
|
git clone https://github.com/gcarq/freqtrade.git
|
||||||
```
|
```
|
||||||
2. (Optional) Checkout the develop branch
|
|
||||||
|
#### 1.2. (Optional) Checkout the develop branch
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout develop
|
git checkout develop
|
||||||
```
|
```
|
||||||
3. Go into the new directory
|
|
||||||
|
#### 1.3. Go into the new directory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
```
|
```
|
||||||
4. Copy `config.sample` to `config.json`
|
|
||||||
```bash
|
|
||||||
cp config.json.example config.json
|
|
||||||
```
|
|
||||||
To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page
|
|
||||||
5. Create your DB file (Optional, the bot will create it if it is missing)
|
|
||||||
```bash
|
|
||||||
# For Production
|
|
||||||
touch tradesv3.sqlite
|
|
||||||
|
|
||||||
# For Dry-run
|
#### 1.4. Copy `config.json.example` to `config.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -n config.json.example config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
|
||||||
|
|
||||||
|
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
|
||||||
|
|
||||||
|
Production
|
||||||
|
```bash
|
||||||
|
touch tradesv3.sqlite
|
||||||
|
````
|
||||||
|
|
||||||
|
Dry-Run
|
||||||
|
```bash
|
||||||
touch tradesv3.dryrun.sqlite
|
touch tradesv3.dryrun.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Build the docker image
|
|
||||||
|
### 2. Build the Docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
docker build -t freqtrade .
|
docker build -t freqtrade .
|
||||||
```
|
```
|
||||||
|
|
||||||
For security reasons, your configuration file will not be included in the
|
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
|
||||||
image, you will need to bind mount it. It is also advised to bind mount
|
|
||||||
a sqlite database file (see the "5. Run a restartable docker image"
|
|
||||||
section) to keep it between updates.
|
|
||||||
|
|
||||||
### 3. Verify the docker image
|
|
||||||
After build process you can verify that the image was created with:
|
### 3. Verify the Docker image
|
||||||
```
|
|
||||||
|
After the build process you can verify that the image was created with:
|
||||||
|
|
||||||
|
```bash
|
||||||
docker images
|
docker images
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Run the docker image
|
|
||||||
You can run a one-off container that is immediately deleted upon exiting with
|
|
||||||
the following command (config.json must be in the current working directory):
|
|
||||||
|
|
||||||
```
|
### 4. Run the Docker image
|
||||||
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
|
||||||
|
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the database will be created inside the docker instance
|
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
|
||||||
and will be lost when you will refresh your image.
|
|
||||||
|
|
||||||
### 5. Run a restartable docker image
|
### 5. Run a restartable docker image
|
||||||
To run a restartable instance in the background (feel free to place your
|
|
||||||
configuration and database files wherever it feels comfortable on your
|
|
||||||
filesystem).
|
|
||||||
|
|
||||||
**5.1. Move your config file and database**
|
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
|
||||||
|
|
||||||
|
#### 5.1. Move your config file and database
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir ~/.freqtrade
|
mkdir ~/.freqtrade
|
||||||
mv config.json ~/.freqtrade
|
mv config.json ~/.freqtrade
|
||||||
mv tradesv3.sqlite ~/.freqtrade
|
mv tradesv3.sqlite ~/.freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
**5.2. Run the docker image**
|
#### 5.2. Run the docker image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name freqtrade \
|
--name freqtrade \
|
||||||
|
-v /etc/localtime:/etc/localtime:ro \
|
||||||
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
-v ~/.freqtrade/config.json:/freqtrade/config.json \
|
||||||
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
|
||||||
freqtrade
|
freqtrade
|
||||||
```
|
```
|
||||||
If you are using `dry_run=True` it's not necessary to mount
|
|
||||||
`tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you
|
|
||||||
plan to use the dry run mode with the param `--dry-run-db`.
|
|
||||||
|
|
||||||
|
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you plan to use the dry run mode with the param `--dry-run-db`.
|
||||||
|
|
||||||
### 6. Monitor your Docker instance
|
### 6. Monitor your Docker instance
|
||||||
|
|
||||||
You can then use the following commands to monitor and manage your container:
|
You can then use the following commands to monitor and manage your container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -116,35 +172,39 @@ docker stop freqtrade
|
|||||||
docker start freqtrade
|
docker start freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
You do not need to rebuild the image for configuration changes, it will
|
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
suffice to edit `config.json` and restart the container.
|
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## Custom Installation
|
||||||
|
|
||||||
|
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
# Linux / MacOS
|
|
||||||
## 1. Requirements
|
|
||||||
Click each one for install guide:
|
Click each one for install guide:
|
||||||
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/),
|
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
||||||
note the bot was not tested on Python >= 3.7.x
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
|
||||||
|
|
||||||
## 2. First install required packages
|
|
||||||
This bot require Python 3.6 and TA-LIB
|
|
||||||
|
|
||||||
### 2.1 Linux - Ubuntu 16.04
|
### Linux - Ubuntu 16.04
|
||||||
|
|
||||||
|
#### 1. Install Python 3.6, Git, and wget
|
||||||
|
|
||||||
**2.1.1. Install Python 3.6, Git, and wget**
|
|
||||||
```bash
|
```bash
|
||||||
sudo add-apt-repository ppa:jonathonf/python-3.6
|
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
||||||
```
|
```
|
||||||
|
|
||||||
**2.1.2. Install TA-LIB**
|
#### 2. Install TA-Lib
|
||||||
|
|
||||||
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
||||||
```
|
|
||||||
|
```bash
|
||||||
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||||
tar xvzf ta-lib-0.4.0-src.tar.gz
|
tar xvzf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib
|
cd ta-lib
|
||||||
@ -155,29 +215,58 @@ cd ..
|
|||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
||||||
**2.1.3. [Optional] Install MongoDB**
|
#### 3. [Optional] Install MongoDB
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install mongodb-org
|
sudo apt-get install mongodb-org
|
||||||
```
|
```
|
||||||
Complete tutorial on [Digital Ocean: How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04)
|
|
||||||
|
|
||||||
### 2.2. Linux - Other distro
|
> 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).
|
||||||
If you are on a different Linux OS you maybe have to adapt things like:
|
|
||||||
|
|
||||||
- package manager (for example yum instead of apt-get)
|
#### 4. Install FreqTrade
|
||||||
- package names
|
|
||||||
|
|
||||||
### 2.3. MacOS installation
|
Clone the git repository:
|
||||||
|
|
||||||
**2.3.1. Install Python 3.6, git and wget**
|
|
||||||
```bash
|
```bash
|
||||||
brew install python3 git wget
|
git clone https://github.com/gcarq/freqtrade.git
|
||||||
```
|
```
|
||||||
|
|
||||||
**2.3.2. [Optional] Install MongoDB**
|
Optionally checkout the develop branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout develop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
|
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
|
||||||
|
After that you can start the daemon with:
|
||||||
|
```bash
|
||||||
|
systemctl --user start freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger "$USER"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
#### 1. Install Python 3.6, git, wget and ta-lib
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install python3 git wget ta-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. [Optional] Install MongoDB
|
||||||
|
|
||||||
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
Install MongoDB if you plan to optimize your strategy with Hyperopt.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
|
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
|
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
|
||||||
@ -186,49 +275,63 @@ cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
|
|||||||
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
|
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Clone the repo
|
#### 3. Install FreqTrade
|
||||||
The following steps are made for Linux/mac environment
|
|
||||||
1. Clone the git `git clone https://github.com/gcarq/freqtrade.git`
|
Clone the git repository:
|
||||||
2. (Optional) Checkout the develop branch `git checkout develop`
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/gcarq/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally checkout the develop branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout develop
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Setup Config and virtual env
|
||||||
|
|
||||||
|
#### 1. Initialize the configuration
|
||||||
|
|
||||||
## 4. Prepare the bot
|
|
||||||
```bash
|
```bash
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
```
|
```
|
||||||
To edit the config please refer to [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
|
||||||
|
|
||||||
## 5. Setup your virtual env
|
> *To edit the config please refer to [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).*
|
||||||
|
|
||||||
|
|
||||||
|
#### 2. Setup your Python virtual environment (virtualenv)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3.6 -m venv .env
|
python3.6 -m venv .env
|
||||||
source .env/bin/activate
|
source .env/bin/activate
|
||||||
|
pip3.6 install --upgrade pip
|
||||||
pip3.6 install -r requirements.txt
|
pip3.6 install -r requirements.txt
|
||||||
pip3.6 install -e .
|
pip3.6 install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. Run the bot
|
#### 3. Run the Bot
|
||||||
If this is the first time you run the bot, ensure you are running it
|
|
||||||
in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3.6 ./freqtrade/main.py -c config.json
|
python3.6 ./freqtrade/main.py -c config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced Linux
|
------
|
||||||
**systemd service file**
|
|
||||||
Copy `./freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`)
|
|
||||||
and update `WorkingDirectory` and `ExecStart` to match your setup.
|
|
||||||
After that you can start the daemon with:
|
|
||||||
```bash
|
|
||||||
systemctl --user start freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
# Windows
|
## Windows
|
||||||
We do recommend Windows users to use [Docker](#docker) this will work
|
|
||||||
much easier and smoother (also safer).
|
We recommend that Windows users use [Docker](#docker) as this will work
|
||||||
|
much easier and smoother (also more secure).
|
||||||
|
|
||||||
|
### Install freqtrade
|
||||||
|
|
||||||
|
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
#copy paste config.json to \path\freqtrade-develop\freqtrade
|
|
||||||
>cd \path\freqtrade-develop
|
>cd \path\freqtrade-develop
|
||||||
>python -m venv .env
|
>python -m venv .env
|
||||||
>cd .env\Scripts
|
>cd .env\Scripts
|
||||||
@ -239,8 +342,9 @@ much easier and smoother (also safer).
|
|||||||
>cd freqtrade
|
>cd freqtrade
|
||||||
>python main.py
|
>python main.py
|
||||||
```
|
```
|
||||||
*Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)*
|
|
||||||
|
|
||||||
## Next step
|
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)
|
||||||
Now you have an environment ready, the next step is to
|
|
||||||
[configure your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).
|
|
||||||
|
Now you have an environment ready, the next step is
|
||||||
|
[Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)...
|
||||||
|
77
docs/plotting.md
Normal file
77
docs/plotting.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Plotting
|
||||||
|
This page explains how to plot prices, indicator, profits.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Plot price and indicators](#plot-price-and-indicators)
|
||||||
|
- [Plot profit](#plot-profit)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Plotting scripts use Plotly library. Install/upgrade it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install --upgrade plotly
|
||||||
|
```
|
||||||
|
|
||||||
|
At least version 2.3.0 is required.
|
||||||
|
|
||||||
|
## Plot price and indicators
|
||||||
|
Usage for the price plotter:
|
||||||
|
|
||||||
|
```
|
||||||
|
script/plot_dataframe.py [-h] [-p pair] [--live]
|
||||||
|
```
|
||||||
|
|
||||||
|
Example
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -p BTC_ETH
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-p` pair argument, can be used to specify what
|
||||||
|
pair you would like to plot.
|
||||||
|
|
||||||
|
**Advanced use**
|
||||||
|
|
||||||
|
To plot the current live price use the `--live` flag:
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -p BTC_ETH --live
|
||||||
|
```
|
||||||
|
|
||||||
|
To plot a timerange (to zoom in):
|
||||||
|
```
|
||||||
|
python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
|
||||||
|
```
|
||||||
|
Timerange doesn't work with live data.
|
||||||
|
|
||||||
|
|
||||||
|
## Plot profit
|
||||||
|
|
||||||
|
The profit plotter show a picture with three plots:
|
||||||
|
1) Average closing price for all pairs
|
||||||
|
2) The summarized profit made by backtesting.
|
||||||
|
Note that this is not the real-world profit, but
|
||||||
|
more of an estimate.
|
||||||
|
3) Each pair individually profit
|
||||||
|
|
||||||
|
The first graph is good to get a grip of how the overall market
|
||||||
|
progresses.
|
||||||
|
|
||||||
|
The second graph will show how you algorithm works or doesnt.
|
||||||
|
Perhaps you want an algorithm that steadily makes small profits,
|
||||||
|
or one that acts less seldom, but makes big swings.
|
||||||
|
|
||||||
|
The third graph can be useful to spot outliers, events in pairs
|
||||||
|
that makes profit spikes.
|
||||||
|
|
||||||
|
Usage for the profit plotter:
|
||||||
|
|
||||||
|
```
|
||||||
|
script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-p` pair argument, can be used to plot a single pair
|
||||||
|
|
||||||
|
Example
|
||||||
|
```
|
||||||
|
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC
|
||||||
|
```
|
@ -15,7 +15,7 @@ The only things you need is a working Telegram bot and its API token.
|
|||||||
Below we explain how to create your Telegram Bot, and how to get your
|
Below we explain how to create your Telegram Bot, and how to get your
|
||||||
Telegram user id.
|
Telegram user id.
|
||||||
|
|
||||||
### 1. Create your instagram bot
|
### 1. Create your Telegram bot
|
||||||
**1.1. Start a chat with https://telegram.me/BotFather**
|
**1.1. Start a chat with https://telegram.me/BotFather**
|
||||||
**1.2. Send the message** `/newbot`
|
**1.2. Send the message** `/newbot`
|
||||||
*BotFather response:*
|
*BotFather response:*
|
||||||
@ -39,8 +39,10 @@ Use this token to access the HTTP API:
|
|||||||
|
|
||||||
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
|
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
|
||||||
```
|
```
|
||||||
|
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
|
||||||
|
|
||||||
### 2. Get your user id
|
### 2. Get your user id
|
||||||
**2.1. Talk to https://telegram.me/userinfobot**
|
**2.1. Talk to https://telegram.me/userinfobot**
|
||||||
**2.2. Get your "Id", you will use it for the config parameter
|
**2.2. Get your "Id", you will use it for the config parameter
|
||||||
`chat_id`.**
|
`chat_id`.**
|
||||||
|
|
||||||
|
@ -67,6 +67,18 @@ SET is_open=0, close_date='2017-12-20 03:08:45.103418', close_rate=0.19638016, c
|
|||||||
WHERE id=31;
|
WHERE id=31;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Insert manually a new trade
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT
|
||||||
|
INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date)
|
||||||
|
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```sql
|
||||||
|
INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
|
||||||
|
```
|
||||||
|
|
||||||
## Fix wrong fees in the table
|
## Fix wrong fees in the table
|
||||||
If your DB was created before
|
If your DB was created before
|
||||||
|
@ -15,7 +15,7 @@ official commands. You can ask at any moment for help with `/help`.
|
|||||||
| Command | Default | Description |
|
| Command | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `/start` | | Starts the trader
|
| `/start` | | Starts the trader
|
||||||
| `/stop` | | Starts the trader
|
| `/stop` | | Stops the trader
|
||||||
| `/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
|
||||||
@ -127,3 +127,14 @@ Day Profit BTC Profit USD
|
|||||||
|
|
||||||
## /version
|
## /version
|
||||||
> **Version:** `0.14.3`
|
> **Version:** `0.14.3`
|
||||||
|
|
||||||
|
### using proxy with telegram
|
||||||
|
in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace
|
||||||
|
```
|
||||||
|
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
with
|
||||||
|
```
|
||||||
|
self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0)
|
||||||
|
```
|
||||||
|
@ -2,314 +2,213 @@
|
|||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(Enum):
|
||||||
""" Enum to distinguish between buy and sell signals """
|
"""
|
||||||
|
Enum to distinguish between buy and sell signals
|
||||||
|
"""
|
||||||
BUY = "buy"
|
BUY = "buy"
|
||||||
SELL = "sell"
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
class Analyze(object):
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given ticker history
|
Analyze class contains everything the bot need to determine if the situation is good for
|
||||||
:param ticker: See exchange.get_ticker_history
|
buying or selling.
|
||||||
:return: DataFrame
|
|
||||||
"""
|
"""
|
||||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
def __init__(self, config: dict) -> None:
|
||||||
frame = DataFrame(ticker) \
|
"""
|
||||||
.drop('BV', 1) \
|
Init Analyze
|
||||||
.rename(columns=columns)
|
:param config: Bot configuration (use the one from Configuration())
|
||||||
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
"""
|
||||||
frame.sort_values('date', inplace=True)
|
self.config = config
|
||||||
return frame
|
self.strategy = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Analyses the trend for the given ticker history
|
||||||
|
:param ticker: See exchange.get_ticker_history
|
||||||
|
:return: DataFrame
|
||||||
|
"""
|
||||||
|
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
||||||
|
frame = DataFrame(ticker).rename(columns=columns)
|
||||||
|
if 'BV' in frame:
|
||||||
|
frame.drop('BV', axis=1, inplace=True)
|
||||||
|
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
you are using. Let uncomment only the indicator you are using in your strategies
|
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
'close': 'last',
|
||||||
"""
|
'high': 'max',
|
||||||
|
'low': 'min',
|
||||||
|
'open': 'first',
|
||||||
|
'volume': 'max',
|
||||||
|
})
|
||||||
|
return frame
|
||||||
|
|
||||||
# Momentum Indicator
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
# ------------------------------------
|
"""
|
||||||
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
# ADX
|
Performance Note: For the best performance be frugal on the number of indicators
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
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.
|
||||||
|
"""
|
||||||
|
return self.strategy.populate_indicators(dataframe=dataframe)
|
||||||
|
|
||||||
# Awesome oscillator
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
"""
|
||||||
"""
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
:param dataframe: DataFrame
|
||||||
dataframe['cci'] = ta.CCI(dataframe)
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
# MACD
|
return self.strategy.populate_buy_trend(dataframe=dataframe)
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# MFI
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
||||||
|
|
||||||
# Minus Directional Indicator / Movement
|
def get_ticker_interval(self) -> int:
|
||||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
"""
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
Return ticker interval to use
|
||||||
|
:return: Ticker interval value to use
|
||||||
|
"""
|
||||||
|
return self.strategy.ticker_interval
|
||||||
|
|
||||||
# Plus Directional Indicator / Movement
|
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
||||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
"""
|
||||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
Parses the given ticker history and returns a populated DataFrame
|
||||||
"""
|
add several TA indicators and buy signal to it
|
||||||
# ROC
|
:return DataFrame with ticker data and indicator data
|
||||||
dataframe['roc'] = ta.ROC(dataframe)
|
"""
|
||||||
"""
|
dataframe = self.parse_ticker_dataframe(ticker_history)
|
||||||
# RSI
|
dataframe = self.populate_indicators(dataframe)
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
dataframe = self.populate_buy_trend(dataframe)
|
||||||
"""
|
dataframe = self.populate_sell_trend(dataframe)
|
||||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
return dataframe
|
||||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
|
||||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
|
||||||
|
|
||||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
|
||||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
"""
|
||||||
|
Calculates current signal based several technical analysis indicators
|
||||||
|
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||||
|
:param interval: Interval to use (in min)
|
||||||
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||||
|
"""
|
||||||
|
ticker_hist = get_ticker_history(pair, interval)
|
||||||
|
if not ticker_hist:
|
||||||
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
|
return False, False
|
||||||
|
|
||||||
# Stoch
|
try:
|
||||||
stoch = ta.STOCH(dataframe)
|
dataframe = self.analyze_ticker(ticker_hist)
|
||||||
dataframe['slowd'] = stoch['slowd']
|
except ValueError as error:
|
||||||
dataframe['slowk'] = stoch['slowk']
|
logger.warning(
|
||||||
"""
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
# Stoch fast
|
pair,
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
str(error)
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
)
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
return False, False
|
||||||
"""
|
except Exception as error:
|
||||||
# Stoch RSI
|
logger.exception(
|
||||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
pair,
|
||||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
str(error)
|
||||||
"""
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
# Overlap Studies
|
if dataframe.empty:
|
||||||
# ------------------------------------
|
logger.warning('Empty dataframe for pair %s', pair)
|
||||||
|
return False, False
|
||||||
|
|
||||||
# Previous Bollinger bands
|
latest = dataframe.iloc[-1]
|
||||||
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
|
||||||
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
|
||||||
# and use middle band instead.
|
|
||||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
|
||||||
"""
|
|
||||||
# Bollinger bands
|
|
||||||
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']
|
|
||||||
"""
|
|
||||||
|
|
||||||
# EMA - Exponential Moving Average
|
# Check if dataframe is out of date
|
||||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
signal_date = arrow.get(latest['date'])
|
||||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
|
||||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
logger.warning(
|
||||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
pair,
|
||||||
|
(arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
# SAR Parabol
|
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
logger.debug(
|
||||||
|
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||||
|
latest['date'],
|
||||||
|
pair,
|
||||||
|
str(buy),
|
||||||
|
str(sell)
|
||||||
|
)
|
||||||
|
return buy, sell
|
||||||
|
|
||||||
# SMA - Simple Moving Average
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
|
||||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
"""
|
||||||
|
This function evaluate if on the condition required to trigger a sell has been reached
|
||||||
|
if the threshold is reached and updates the trade record.
|
||||||
|
:return: True if trade should be sold, False otherwise
|
||||||
|
"""
|
||||||
|
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||||
|
if self.min_roi_reached(trade=trade, current_rate=rate, current_time=date):
|
||||||
|
logger.debug('Required profit reached. Selling..')
|
||||||
|
return True
|
||||||
|
|
||||||
# TEMA - Triple Exponential Moving Average
|
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
if self.config.get('experimental', {}).get('sell_profit_only', False):
|
||||||
|
logger.debug('Checking if trade is profitable..')
|
||||||
|
if trade.calc_profit(rate=rate) <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
# Cycle Indicator
|
if sell and not buy and self.config.get('experimental', {}).get('use_sell_signal', False):
|
||||||
# ------------------------------------
|
logger.debug('Sell signal received. Selling..')
|
||||||
# Hilbert Transform Indicator - SineWave
|
return True
|
||||||
hilbert = ta.HT_SINE(dataframe)
|
|
||||||
dataframe['htsine'] = hilbert['sine']
|
|
||||||
dataframe['htleadsine'] = hilbert['leadsine']
|
|
||||||
|
|
||||||
# Pattern Recognition - Bullish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
"""
|
|
||||||
# Hammer: values [0, 100]
|
|
||||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
|
||||||
|
|
||||||
# Inverted Hammer: values [0, 100]
|
|
||||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
|
||||||
|
|
||||||
# Dragonfly Doji: values [0, 100]
|
|
||||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
|
||||||
|
|
||||||
# Piercing Line: values [0, 100]
|
|
||||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
|
||||||
|
|
||||||
# Morningstar: values [0, 100]
|
|
||||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
|
||||||
|
|
||||||
# Three White Soldiers: values [0, 100]
|
|
||||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Pattern Recognition - Bearish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
"""
|
|
||||||
# Hanging Man: values [0, 100]
|
|
||||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
|
||||||
|
|
||||||
# Shooting Star: values [0, 100]
|
|
||||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
|
||||||
|
|
||||||
# Gravestone Doji: values [0, 100]
|
|
||||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
|
||||||
|
|
||||||
# Dark Cloud Cover: values [0, 100]
|
|
||||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
|
||||||
|
|
||||||
# Evening Doji Star: values [0, 100]
|
|
||||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
|
||||||
|
|
||||||
# Evening Star: values [0, 100]
|
|
||||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
|
||||||
# ------------------------------------
|
|
||||||
"""
|
|
||||||
# Three Line Strike: values [0, -100, 100]
|
|
||||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
|
||||||
|
|
||||||
# Spinning Top: values [0, -100, 100]
|
|
||||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
|
||||||
|
|
||||||
# Engulfing: values [0, -100, 100]
|
|
||||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
|
||||||
|
|
||||||
# Harami: values [0, -100, 100]
|
|
||||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
|
||||||
|
|
||||||
# Three Outside Up/Down: values [0, -100, 100]
|
|
||||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
|
||||||
|
|
||||||
# Three Inside Up/Down: values [0, -100, 100]
|
|
||||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Chart type
|
|
||||||
# ------------------------------------
|
|
||||||
"""
|
|
||||||
# Heikinashi stategy
|
|
||||||
heikinashi = qtpylib.heikinashi(dataframe)
|
|
||||||
dataframe['ha_open'] = heikinashi['open']
|
|
||||||
dataframe['ha_close'] = heikinashi['close']
|
|
||||||
dataframe['ha_high'] = heikinashi['high']
|
|
||||||
dataframe['ha_low'] = heikinashi['low']
|
|
||||||
"""
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def populate_buy_trend(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['rsi'] < 35) &
|
|
||||||
(dataframe['fastd'] < 35) &
|
|
||||||
(dataframe['adx'] > 30) &
|
|
||||||
(dataframe['plus_di'] > 0.5)
|
|
||||||
) |
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 65) &
|
|
||||||
(dataframe['plus_di'] > 0.5)
|
|
||||||
),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def populate_sell_trend(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[
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
|
||||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
|
||||||
) &
|
|
||||||
(dataframe['adx'] > 10) &
|
|
||||||
(dataframe['minus_di'] > 0)
|
|
||||||
) |
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 70) &
|
|
||||||
(dataframe['minus_di'] > 0.5)
|
|
||||||
),
|
|
||||||
'sell'] = 1
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Parses the given ticker history and returns a populated DataFrame
|
|
||||||
add several TA indicators and buy signal to it
|
|
||||||
:return DataFrame with ticker data and indicator data
|
|
||||||
"""
|
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
|
||||||
dataframe = populate_indicators(dataframe)
|
|
||||||
dataframe = populate_buy_trend(dataframe)
|
|
||||||
dataframe = populate_sell_trend(dataframe)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
|
|
||||||
def get_signal(pair: str, signal: SignalType) -> bool:
|
|
||||||
"""
|
|
||||||
Calculates current signal based several technical analysis indicators
|
|
||||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
|
||||||
:return: True if pair is good for buying, False otherwise
|
|
||||||
"""
|
|
||||||
ticker_hist = get_ticker_history(pair)
|
|
||||||
if not ticker_hist:
|
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
dataframe = analyze_ticker(ticker_hist)
|
"""
|
||||||
except ValueError as ex:
|
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||||
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
sell
|
||||||
return False
|
:return True if bot should sell at current rate
|
||||||
except Exception as ex:
|
"""
|
||||||
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
||||||
|
logger.debug('Stop loss hit.')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if time matches and current rate is above threshold
|
||||||
|
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
|
||||||
|
for duration, threshold in self.strategy.minimal_roi.items():
|
||||||
|
if time_diff <= duration:
|
||||||
|
return False
|
||||||
|
if current_profit > threshold:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if dataframe.empty:
|
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||||
return False
|
"""
|
||||||
|
Creates a dataframe and populates indicators for given ticker data
|
||||||
latest = dataframe.iloc[-1]
|
"""
|
||||||
|
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
||||||
# Check if dataframe is out of date
|
for pair, pair_data in tickerdata.items()}
|
||||||
signal_date = arrow.get(latest['date'])
|
|
||||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
|
||||||
return False
|
|
||||||
|
|
||||||
result = latest[signal.value] == 1
|
|
||||||
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
|
||||||
return result
|
|
||||||
|
257
freqtrade/arguments.py
Normal file
257
freqtrade/arguments.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
"""
|
||||||
|
This module contains the argument manager class
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
|
from freqtrade import __version__, constants
|
||||||
|
|
||||||
|
|
||||||
|
class Arguments(object):
|
||||||
|
"""
|
||||||
|
Arguments Class. Manage the arguments received by the cli
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, args: List[str], description: str):
|
||||||
|
self.args = args
|
||||||
|
self.parsed_arg = None
|
||||||
|
self.parser = argparse.ArgumentParser(description=description)
|
||||||
|
|
||||||
|
def _load_args(self) -> None:
|
||||||
|
self.common_args_parser()
|
||||||
|
self._build_subcommands()
|
||||||
|
|
||||||
|
def get_parsed_arg(self) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Return the list of arguments
|
||||||
|
:return: List[str] List of arguments
|
||||||
|
"""
|
||||||
|
if self.parsed_arg is None:
|
||||||
|
self._load_args()
|
||||||
|
self.parsed_arg = self.parse_args()
|
||||||
|
|
||||||
|
return self.parsed_arg
|
||||||
|
|
||||||
|
def parse_args(self) -> argparse.Namespace:
|
||||||
|
"""
|
||||||
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
|
"""
|
||||||
|
parsed_arg = self.parser.parse_args(self.args)
|
||||||
|
|
||||||
|
return parsed_arg
|
||||||
|
|
||||||
|
def common_args_parser(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given common arguments and returns them as a parsed object.
|
||||||
|
"""
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-v', '--verbose',
|
||||||
|
help='be verbose',
|
||||||
|
action='store_const',
|
||||||
|
dest='loglevel',
|
||||||
|
const=logging.DEBUG,
|
||||||
|
default=logging.INFO,
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version='%(prog)s {}'.format(__version__),
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-c', '--config',
|
||||||
|
help='specify configuration file (default: %(default)s)',
|
||||||
|
dest='config',
|
||||||
|
default='config.json',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-d', '--datadir',
|
||||||
|
help='path to backtest data (default: %(default)s',
|
||||||
|
dest='datadir',
|
||||||
|
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-s', '--strategy',
|
||||||
|
help='specify strategy class name (default: %(default)s)',
|
||||||
|
dest='strategy',
|
||||||
|
default='DefaultStrategy',
|
||||||
|
type=str,
|
||||||
|
metavar='NAME',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--strategy-path',
|
||||||
|
help='specify additional strategy lookup path',
|
||||||
|
dest='strategy_path',
|
||||||
|
type=str,
|
||||||
|
metavar='PATH',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--dynamic-whitelist',
|
||||||
|
help='dynamically generate and update whitelist \
|
||||||
|
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
||||||
|
dest='dynamic_whitelist',
|
||||||
|
const=constants.DYNAMIC_WHITELIST,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
nargs='?',
|
||||||
|
)
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--dry-run-db',
|
||||||
|
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
||||||
|
instead of memory DB. Work only if dry_run is enabled.',
|
||||||
|
action='store_true',
|
||||||
|
dest='dry_run_db',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def backtesting_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Backtesting scripts.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'-l', '--live',
|
||||||
|
help='using live data',
|
||||||
|
action='store_true',
|
||||||
|
dest='live',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-r', '--refresh-pairs-cached',
|
||||||
|
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
||||||
|
Use it if you want to run your backtesting with up-to-date data.',
|
||||||
|
action='store_true',
|
||||||
|
dest='refresh_pairs',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--export',
|
||||||
|
help='export backtest results, argument are: trades\
|
||||||
|
Example --export=trades',
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
dest='export',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--realistic-simulation',
|
||||||
|
help='uses max_open_trades from config to simulate real world limitations',
|
||||||
|
action='store_true',
|
||||||
|
dest='realistic_simulation',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--timerange',
|
||||||
|
help='specify what timerange of data to use.',
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
dest='timerange',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for Hyperopt scripts.
|
||||||
|
"""
|
||||||
|
parser.add_argument(
|
||||||
|
'-e', '--epochs',
|
||||||
|
help='specify number of epochs (default: %(default)d)',
|
||||||
|
dest='epochs',
|
||||||
|
default=constants.HYPEROPT_EPOCH,
|
||||||
|
type=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(
|
||||||
|
'-s', '--spaces',
|
||||||
|
help='Specify which parameters to hyperopt. Space separate list. \
|
||||||
|
Default: %(default)s',
|
||||||
|
choices=['all', 'buy', 'roi', 'stoploss'],
|
||||||
|
default='all',
|
||||||
|
nargs='+',
|
||||||
|
dest='spaces',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_subcommands(self) -> None:
|
||||||
|
"""
|
||||||
|
Builds and attaches all subcommands
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
from freqtrade.optimize import backtesting, hyperopt
|
||||||
|
|
||||||
|
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||||
|
|
||||||
|
# Add backtesting subcommand
|
||||||
|
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
||||||
|
backtesting_cmd.set_defaults(func=backtesting.start)
|
||||||
|
self.optimizer_shared_options(backtesting_cmd)
|
||||||
|
self.backtesting_options(backtesting_cmd)
|
||||||
|
|
||||||
|
# Add hyperopt subcommand
|
||||||
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||||
|
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||||
|
self.optimizer_shared_options(hyperopt_cmd)
|
||||||
|
self.hyperopt_options(hyperopt_cmd)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
|
||||||
|
"""
|
||||||
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
|
:param text: value from --timerange
|
||||||
|
:return: Start and End range period
|
||||||
|
"""
|
||||||
|
if text is None:
|
||||||
|
return None
|
||||||
|
syntax = [(r'^-(\d{8})$', (None, 'date')),
|
||||||
|
(r'^(\d{8})-$', ('date', None)),
|
||||||
|
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
|
||||||
|
(r'^(-\d+)$', (None, 'line')),
|
||||||
|
(r'^(\d+)-$', ('line', None)),
|
||||||
|
(r'^(\d+)-(\d+)$', ('index', 'index'))]
|
||||||
|
for rex, stype in syntax:
|
||||||
|
# Apply the regular expression to text
|
||||||
|
match = re.match(rex, text)
|
||||||
|
if match: # Regex has matched
|
||||||
|
rvals = match.groups()
|
||||||
|
index = 0
|
||||||
|
start = None
|
||||||
|
stop = None
|
||||||
|
if stype[0]:
|
||||||
|
start = rvals[index]
|
||||||
|
if stype[0] != 'date':
|
||||||
|
start = int(start)
|
||||||
|
index += 1
|
||||||
|
if stype[1]:
|
||||||
|
stop = rvals[index]
|
||||||
|
if stype[1] != 'date':
|
||||||
|
stop = int(stop)
|
||||||
|
return stype, start, stop
|
||||||
|
raise Exception('Incorrect syntax for timerange "%s"' % text)
|
||||||
|
|
||||||
|
def scripts_options(self) -> None:
|
||||||
|
"""
|
||||||
|
Parses given arguments for plot scripts.
|
||||||
|
"""
|
||||||
|
self.parser.add_argument(
|
||||||
|
'-p', '--pair',
|
||||||
|
help='Show profits for only this pairs. Pairs are comma-separated.',
|
||||||
|
dest='pair',
|
||||||
|
default=None
|
||||||
|
)
|
208
freqtrade/configuration.py
Normal file
208
freqtrade/configuration.py
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
"""
|
||||||
|
This module contains the configuration class
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from jsonschema import Draft4Validator, validate
|
||||||
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
|
|
||||||
|
from freqtrade import constants
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(object):
|
||||||
|
"""
|
||||||
|
Class to read and init the bot configuration
|
||||||
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
||||||
|
"""
|
||||||
|
def __init__(self, args: Namespace) -> None:
|
||||||
|
self.args = args
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
def load_config(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load the bot configuration
|
||||||
|
:return: Configuration dictionary
|
||||||
|
"""
|
||||||
|
logger.info('Using config: %s ...', self.args.config)
|
||||||
|
config = self._load_config_file(self.args.config)
|
||||||
|
|
||||||
|
# Set strategy if not specified in config and or if it's non default
|
||||||
|
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
||||||
|
config.update({'strategy': self.args.strategy})
|
||||||
|
|
||||||
|
if self.args.strategy_path:
|
||||||
|
config.update({'strategy_path': self.args.strategy_path})
|
||||||
|
|
||||||
|
# Load Common configuration
|
||||||
|
config = self._load_common_config(config)
|
||||||
|
|
||||||
|
# Load Backtesting
|
||||||
|
config = self._load_backtesting_config(config)
|
||||||
|
|
||||||
|
# Load Hyperopt
|
||||||
|
config = self._load_hyperopt_config(config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path) as file:
|
||||||
|
conf = json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.critical(
|
||||||
|
'Config file "%s" not found. Please create your config file',
|
||||||
|
path
|
||||||
|
)
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
if 'internals' not in conf:
|
||||||
|
conf['internals'] = {}
|
||||||
|
logger.info('Validating configuration ...')
|
||||||
|
|
||||||
|
return self._validate_config(conf)
|
||||||
|
|
||||||
|
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load common configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
if 'loglevel' in self.args and self.args.loglevel:
|
||||||
|
config.update({'loglevel': self.args.loglevel})
|
||||||
|
logging.basicConfig(
|
||||||
|
level=config['loglevel'],
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
logger.info('Log level set to %s', logging.getLevelName(config['loglevel']))
|
||||||
|
|
||||||
|
# Add dynamic_whitelist if found
|
||||||
|
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||||
|
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
|
||||||
|
logger.info(
|
||||||
|
'Parameter --dynamic-whitelist detected. '
|
||||||
|
'Using dynamically generated whitelist. '
|
||||||
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add dry_run_db if found and the bot in dry run
|
||||||
|
if self.args.dry_run_db and config.get('dry_run', False):
|
||||||
|
config.update({'dry_run_db': True})
|
||||||
|
logger.info('Parameter --dry-run-db detected ...')
|
||||||
|
|
||||||
|
if config.get('dry_run_db', False):
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
||||||
|
else:
|
||||||
|
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Backtesting configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If -i/--ticker-interval is used we override the configuration parameter
|
||||||
|
# (that will override the strategy configuration)
|
||||||
|
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
||||||
|
config.update({'ticker_interval': self.args.ticker_interval})
|
||||||
|
logger.info('Parameter -i/--ticker-interval detected ...')
|
||||||
|
logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
|
||||||
|
|
||||||
|
# If -l/--live is used we add it to the configuration
|
||||||
|
if 'live' in self.args and self.args.live:
|
||||||
|
config.update({'live': True})
|
||||||
|
logger.info('Parameter -l/--live detected ...')
|
||||||
|
|
||||||
|
# If --realistic-simulation is used we add it to the configuration
|
||||||
|
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
|
||||||
|
config.update({'realistic_simulation': True})
|
||||||
|
logger.info('Parameter --realistic-simulation detected ...')
|
||||||
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
|
||||||
|
# If --timerange is used we add it to the configuration
|
||||||
|
if 'timerange' in self.args and self.args.timerange:
|
||||||
|
config.update({'timerange': self.args.timerange})
|
||||||
|
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
||||||
|
|
||||||
|
# If --datadir is used we add it to the configuration
|
||||||
|
if 'datadir' in self.args and self.args.datadir:
|
||||||
|
config.update({'datadir': self.args.datadir})
|
||||||
|
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
|
||||||
|
|
||||||
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
||||||
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
||||||
|
config.update({'refresh_pairs': True})
|
||||||
|
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
||||||
|
|
||||||
|
# If --export is used we add it to the configuration
|
||||||
|
if 'export' in self.args and self.args.export:
|
||||||
|
config.update({'export': self.args.export})
|
||||||
|
logger.info('Parameter --export detected: %s ...', self.args.export)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Extract information for sys.argv and load Hyperopt configuration
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
# If --realistic-simulation is used we add it to the configuration
|
||||||
|
if 'epochs' in self.args and self.args.epochs:
|
||||||
|
config.update({'epochs': self.args.epochs})
|
||||||
|
logger.info('Parameter --epochs detected ...')
|
||||||
|
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' in self.args and self.args.spaces:
|
||||||
|
config.update({'spaces': self.args.spaces})
|
||||||
|
logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate the configuration follow the Config Schema
|
||||||
|
:param conf: Config in JSON format
|
||||||
|
:return: Returns the config if valid, otherwise throw an exception
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
validate(conf, constants.CONF_SCHEMA)
|
||||||
|
return conf
|
||||||
|
except ValidationError as exception:
|
||||||
|
logger.fatal(
|
||||||
|
'Invalid configuration. See config.json.example. Reason: %s',
|
||||||
|
exception
|
||||||
|
)
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_config(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return the config. Use this method to get the bot config
|
||||||
|
:return: Dict: Bot config
|
||||||
|
"""
|
||||||
|
if self.config is None:
|
||||||
|
self.config = self.load_config()
|
||||||
|
|
||||||
|
return self.config
|
116
freqtrade/constants.py
Normal file
116
freqtrade/constants.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# pragma pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
bot constants
|
||||||
|
"""
|
||||||
|
DYNAMIC_WHITELIST = 20 # pairs
|
||||||
|
PROCESS_THROTTLE_SECS = 5 # sec
|
||||||
|
TICKER_INTERVAL = 5 # min
|
||||||
|
HYPEROPT_EPOCH = 100 # epochs
|
||||||
|
RETRY_TIMEOUT = 30 # sec
|
||||||
|
DEFAULT_STRATEGY = 'DefaultStrategy'
|
||||||
|
|
||||||
|
# Required json-schema for user specified config
|
||||||
|
CONF_SCHEMA = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||||
|
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
|
||||||
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||||
|
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||||
|
'fiat_display_currency': {'type': 'string', 'enum': ['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']},
|
||||||
|
'dry_run': {'type': 'boolean'},
|
||||||
|
'minimal_roi': {
|
||||||
|
'type': 'object',
|
||||||
|
'patternProperties': {
|
||||||
|
'^[0-9.]+$': {'type': 'number'}
|
||||||
|
},
|
||||||
|
'minProperties': 1
|
||||||
|
},
|
||||||
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||||
|
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
||||||
|
'bid_strategy': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'ask_last_balance': {
|
||||||
|
'type': 'number',
|
||||||
|
'minimum': 0,
|
||||||
|
'maximum': 1,
|
||||||
|
'exclusiveMaximum': False
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['ask_last_balance']
|
||||||
|
},
|
||||||
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
|
'experimental': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'use_sell_signal': {'type': 'boolean'},
|
||||||
|
'sell_profit_only': {'type': 'boolean'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'telegram': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'token': {'type': 'string'},
|
||||||
|
'chat_id': {'type': 'string'},
|
||||||
|
},
|
||||||
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
|
},
|
||||||
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
|
'internals': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'process_throttle_secs': {'type': 'number'},
|
||||||
|
'interval': {'type': 'integer'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'definitions': {
|
||||||
|
'exchange': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'key': {'type': 'string'},
|
||||||
|
'secret': {'type': 'string'},
|
||||||
|
'pair_whitelist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
|
'uniqueItems': True
|
||||||
|
},
|
||||||
|
'pair_blacklist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
|
'uniqueItems': True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'anyOf': [
|
||||||
|
{'required': ['exchange']}
|
||||||
|
],
|
||||||
|
'required': [
|
||||||
|
'max_open_trades',
|
||||||
|
'stake_currency',
|
||||||
|
'stake_amount',
|
||||||
|
'fiat_display_currency',
|
||||||
|
'dry_run',
|
||||||
|
'bid_strategy',
|
||||||
|
'telegram'
|
||||||
|
]
|
||||||
|
}
|
@ -139,7 +139,7 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=100, ttl=30))
|
@cached(TTLCache(maxsize=100, ttl=30))
|
||||||
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
|
def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
|
||||||
return _API.get_ticker_history(pair, tick_interval)
|
return _API.get_ticker_history(pair, tick_interval)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex as _Bittrex
|
|
||||||
from bittrex.bittrex import API_V1_1, API_V2_0
|
from bittrex.bittrex import API_V1_1, API_V2_0
|
||||||
|
from bittrex.bittrex import Bittrex as _Bittrex
|
||||||
from requests.exceptions import ContentDecodingError
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
@ -15,20 +14,6 @@ _API: _Bittrex = None
|
|||||||
_API_V2: _Bittrex = None
|
_API_V2: _Bittrex = None
|
||||||
_EXCHANGE_CONF: dict = {}
|
_EXCHANGE_CONF: dict = {}
|
||||||
|
|
||||||
# API socket timeout
|
|
||||||
API_TIMEOUT = 60
|
|
||||||
|
|
||||||
|
|
||||||
def custom_requests(request_url, apisign):
|
|
||||||
"""
|
|
||||||
Set timeout for requests
|
|
||||||
"""
|
|
||||||
return requests.get(
|
|
||||||
request_url,
|
|
||||||
headers={"apisign": apisign},
|
|
||||||
timeout=API_TIMEOUT
|
|
||||||
).json()
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
class Bittrex(Exchange):
|
||||||
"""
|
"""
|
||||||
@ -47,14 +32,12 @@ class Bittrex(Exchange):
|
|||||||
api_secret=_EXCHANGE_CONF['secret'],
|
api_secret=_EXCHANGE_CONF['secret'],
|
||||||
calls_per_second=1,
|
calls_per_second=1,
|
||||||
api_version=API_V1_1,
|
api_version=API_V1_1,
|
||||||
dispatch=custom_requests
|
|
||||||
)
|
)
|
||||||
_API_V2 = _Bittrex(
|
_API_V2 = _Bittrex(
|
||||||
api_key=_EXCHANGE_CONF['key'],
|
api_key=_EXCHANGE_CONF['key'],
|
||||||
api_secret=_EXCHANGE_CONF['secret'],
|
api_secret=_EXCHANGE_CONF['secret'],
|
||||||
calls_per_second=1,
|
calls_per_second=1,
|
||||||
api_version=API_V2_0,
|
api_version=API_V2_0,
|
||||||
dispatch=custom_requests
|
|
||||||
)
|
)
|
||||||
self.cached_ticker = {}
|
self.cached_ticker = {}
|
||||||
|
|
||||||
@ -69,7 +52,7 @@ class Bittrex(Exchange):
|
|||||||
'MIN_TRADE_REQUIREMENT_NOT_MET',
|
'MIN_TRADE_REQUIREMENT_NOT_MET',
|
||||||
]
|
]
|
||||||
if response['message'] in temp_error_messages:
|
if response['message'] in temp_error_messages:
|
||||||
raise ContentDecodingError('Got {}'.format(response['message']))
|
raise ContentDecodingError(response['message'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fee(self) -> float:
|
def fee(self) -> float:
|
||||||
@ -122,13 +105,11 @@ class Bittrex(Exchange):
|
|||||||
raise OperationalException('{message} params=({pair})'.format(
|
raise OperationalException('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
|
keys = ['Bid', 'Ask', 'Last']
|
||||||
if not data.get('result') \
|
if not data.get('result') or\
|
||||||
or not data['result'].get('Bid') \
|
not all(key in data.get('result', {}) for key in keys) or\
|
||||||
or not data['result'].get('Ask') \
|
not all(data.get('result', {})[key] is not None for key in keys):
|
||||||
or not data['result'].get('Last'):
|
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
|
||||||
raise ContentDecodingError('{message} params=({pair})'.format(
|
|
||||||
message='Got invalid response from bittrex',
|
|
||||||
pair=pair))
|
pair=pair))
|
||||||
# Update the pair
|
# Update the pair
|
||||||
self.cached_ticker[pair] = {
|
self.cached_ticker[pair] = {
|
||||||
@ -143,23 +124,27 @@ class Bittrex(Exchange):
|
|||||||
interval = 'oneMin'
|
interval = 'oneMin'
|
||||||
elif tick_interval == 5:
|
elif tick_interval == 5:
|
||||||
interval = 'fiveMin'
|
interval = 'fiveMin'
|
||||||
|
elif tick_interval == 30:
|
||||||
|
interval = 'thirtyMin'
|
||||||
|
elif tick_interval == 60:
|
||||||
|
interval = 'hour'
|
||||||
|
elif tick_interval == 1440:
|
||||||
|
interval = 'Day'
|
||||||
else:
|
else:
|
||||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
raise ValueError('Unknown tick_interval: {}'.format(tick_interval))
|
||||||
|
|
||||||
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
||||||
|
|
||||||
# These sanity check are necessary because bittrex cannot keep their API stable.
|
# These sanity check are necessary because bittrex cannot keep their API stable.
|
||||||
if not data.get('result'):
|
if not data.get('result'):
|
||||||
raise ContentDecodingError('{message} params=({pair})'.format(
|
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
|
||||||
message='Got invalid response from bittrex',
|
|
||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
||||||
for tick in data['result']:
|
for tick in data['result']:
|
||||||
if prop not in tick.keys():
|
if prop not in tick.keys():
|
||||||
raise ContentDecodingError('{message} params=({pair})'.format(
|
raise ContentDecodingError('Required property {} not present '
|
||||||
message='Required property {} not present in response'.format(prop),
|
'in response params=({})'.format(prop, pair))
|
||||||
pair=pair))
|
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
@ -203,21 +188,21 @@ class Bittrex(Exchange):
|
|||||||
data = _API.get_markets()
|
data = _API.get_markets()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message}'.format(message=data['message']))
|
raise OperationalException(data['message'])
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
|
||||||
def get_market_summaries(self) -> List[Dict]:
|
def get_market_summaries(self) -> List[Dict]:
|
||||||
data = _API.get_market_summaries()
|
data = _API.get_market_summaries()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message}'.format(message=data['message']))
|
raise OperationalException(data['message'])
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
||||||
def get_wallet_health(self) -> List[Dict]:
|
def get_wallet_health(self) -> List[Dict]:
|
||||||
data = _API_V2.get_wallet_health()
|
data = _API_V2.get_wallet_health()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
Bittrex._validate_response(data)
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message}'.format(message=data['message']))
|
raise OperationalException(data['message'])
|
||||||
return [{
|
return [{
|
||||||
'Currency': entry['Health']['Currency'],
|
'Currency': entry['Health']['Currency'],
|
||||||
'IsActive': entry['Health']['IsActive'],
|
'IsActive': entry['Health']['IsActive'],
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Module that define classes to convert Crypto-currency to FIAT
|
||||||
|
e.g BTC to USD
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from pymarketcap import Pymarketcap
|
from coinmarketcap import Market
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CryptoFiat():
|
class CryptoFiat(object):
|
||||||
|
"""
|
||||||
|
Object to describe what is the price of Crypto-currency in a FIAT
|
||||||
|
"""
|
||||||
# Constants
|
# Constants
|
||||||
CACHE_DURATION = 6 * 60 * 60 # 6 hours
|
CACHE_DURATION = 6 * 60 * 60 # 6 hours
|
||||||
|
|
||||||
@ -48,7 +56,15 @@ class CryptoFiat():
|
|||||||
return self._expiration - time.time() <= 0
|
return self._expiration - time.time() <= 0
|
||||||
|
|
||||||
|
|
||||||
class CryptoToFiatConverter():
|
class CryptoToFiatConverter(object):
|
||||||
|
"""
|
||||||
|
Main class to initiate Crypto to FIAT.
|
||||||
|
This object contains a list of pair Crypto, FIAT
|
||||||
|
This object is also a Singleton
|
||||||
|
"""
|
||||||
|
__instance = None
|
||||||
|
_coinmarketcap = None
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
SUPPORTED_FIAT = [
|
SUPPORTED_FIAT = [
|
||||||
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
|
||||||
@ -57,12 +73,22 @@ class CryptoToFiatConverter():
|
|||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
CRYPTOMAP = {
|
||||||
try:
|
'BTC': 'bitcoin',
|
||||||
self._coinmarketcap = Pymarketcap()
|
'ETH': 'ethereum',
|
||||||
except BaseException:
|
'USDT': 'thether'
|
||||||
self._coinmarketcap = None
|
}
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if CryptoToFiatConverter.__instance is None:
|
||||||
|
CryptoToFiatConverter.__instance = object.__new__(cls)
|
||||||
|
try:
|
||||||
|
CryptoToFiatConverter._coinmarketcap = Market()
|
||||||
|
except BaseException:
|
||||||
|
CryptoToFiatConverter._coinmarketcap = None
|
||||||
|
return CryptoToFiatConverter.__instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
self._pairs = []
|
self._pairs = []
|
||||||
|
|
||||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
@ -152,12 +178,16 @@ class CryptoToFiatConverter():
|
|||||||
# Check if the fiat convertion you want is supported
|
# Check if the fiat convertion you want is supported
|
||||||
if not self._is_supported_fiat(fiat=fiat_symbol):
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
||||||
|
|
||||||
|
if crypto_symbol not in self.CRYPTOMAP:
|
||||||
|
raise ValueError(
|
||||||
|
'The crypto symbol {} is not supported.'.format(crypto_symbol))
|
||||||
try:
|
try:
|
||||||
return float(
|
return float(
|
||||||
self._coinmarketcap.ticker(
|
self._coinmarketcap.ticker(
|
||||||
currency=crypto_symbol,
|
currency=self.CRYPTOMAP[crypto_symbol],
|
||||||
convert=fiat_symbol
|
convert=fiat_symbol
|
||||||
)['price_' + fiat_symbol.lower()]
|
)[0]['price_' + fiat_symbol.lower()]
|
||||||
)
|
)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
526
freqtrade/freqtradebot.py
Normal file
526
freqtrade/freqtradebot.py
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
"""
|
||||||
|
Freqtrade is the main module of this bot. It contains the class Freqtrade()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Any, Callable
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import requests
|
||||||
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
|
from freqtrade import (
|
||||||
|
DependencyException, OperationalException, exchange, persistence, __version__
|
||||||
|
)
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade import constants
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FreqtradeBot(object):
|
||||||
|
"""
|
||||||
|
Freqtrade is the main class of the bot.
|
||||||
|
This is from here the bot start its logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Init all variables and object the bot need to work
|
||||||
|
:param config: configuration dict, you can use the Configuration.get_config()
|
||||||
|
method to get the config dict.
|
||||||
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Starting freqtrade %s',
|
||||||
|
__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Init bot states
|
||||||
|
self.state = State.STOPPED
|
||||||
|
|
||||||
|
# Init objects
|
||||||
|
self.config = config
|
||||||
|
self.analyze = None
|
||||||
|
self.fiat_converter = None
|
||||||
|
self.rpc = None
|
||||||
|
self.persistence = None
|
||||||
|
self.exchange = None
|
||||||
|
|
||||||
|
self._init_modules(db_url=db_url)
|
||||||
|
|
||||||
|
def _init_modules(self, db_url: Optional[str] = None) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all modules and updates the config
|
||||||
|
:param db_url: database connector string for sqlalchemy (Optional)
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Initialize all modules
|
||||||
|
self.analyze = Analyze(self.config)
|
||||||
|
self.fiat_converter = CryptoToFiatConverter()
|
||||||
|
self.rpc = RPCManager(self)
|
||||||
|
|
||||||
|
persistence.init(self.config, db_url)
|
||||||
|
exchange.init(self.config)
|
||||||
|
|
||||||
|
# Set initial application state
|
||||||
|
initial_state = self.config.get('initial_state')
|
||||||
|
|
||||||
|
if initial_state:
|
||||||
|
self.state = State[initial_state.upper()]
|
||||||
|
else:
|
||||||
|
self.state = State.STOPPED
|
||||||
|
|
||||||
|
def clean(self) -> bool:
|
||||||
|
"""
|
||||||
|
Cleanup the application state und finish all pending tasks
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.rpc.send_msg('*Status:* `Stopping trader...`')
|
||||||
|
logger.info('Stopping trader and cleaning up modules...')
|
||||||
|
self.state = State.STOPPED
|
||||||
|
self.rpc.cleanup()
|
||||||
|
persistence.cleanup()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def worker(self, old_state: None) -> State:
|
||||||
|
"""
|
||||||
|
Trading routine that must be run at each loop
|
||||||
|
:param old_state: the previous service state from the previous call
|
||||||
|
:return: current service state
|
||||||
|
"""
|
||||||
|
# Log state transition
|
||||||
|
state = self.state
|
||||||
|
if state != old_state:
|
||||||
|
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
|
||||||
|
logger.info('Changing state to: %s', state.name)
|
||||||
|
|
||||||
|
if state == State.STOPPED:
|
||||||
|
time.sleep(1)
|
||||||
|
elif state == State.RUNNING:
|
||||||
|
min_secs = self.config.get('internals', {}).get(
|
||||||
|
'process_throttle_secs',
|
||||||
|
constants.PROCESS_THROTTLE_SECS
|
||||||
|
)
|
||||||
|
|
||||||
|
nb_assets = self.config.get('dynamic_whitelist', None)
|
||||||
|
|
||||||
|
self._throttle(func=self._process,
|
||||||
|
min_secs=min_secs,
|
||||||
|
nb_assets=nb_assets)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
|
"""
|
||||||
|
Throttles the given callable that it
|
||||||
|
takes at least `min_secs` to finish execution.
|
||||||
|
:param func: Any callable
|
||||||
|
:param min_secs: minimum execution time in seconds
|
||||||
|
:return: Any
|
||||||
|
"""
|
||||||
|
start = time.time()
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
end = time.time()
|
||||||
|
duration = max(min_secs - (end - start), 0.0)
|
||||||
|
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
||||||
|
time.sleep(duration)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _process(self, nb_assets: Optional[int] = 0) -> bool:
|
||||||
|
"""
|
||||||
|
Queries the persistence layer for open trades and handles them,
|
||||||
|
otherwise a new trade is created.
|
||||||
|
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
||||||
|
:return: True if one or more trades has been created or closed, False otherwise
|
||||||
|
"""
|
||||||
|
state_changed = False
|
||||||
|
try:
|
||||||
|
# Refresh whitelist based on wallet maintenance
|
||||||
|
sanitized_list = self._refresh_whitelist(
|
||||||
|
self._gen_pair_whitelist(
|
||||||
|
self.config['stake_currency']
|
||||||
|
) if nb_assets else self.config['exchange']['pair_whitelist']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep only the subsets of pairs wanted (up to nb_assets)
|
||||||
|
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
||||||
|
self.config['exchange']['pair_whitelist'] = final_list
|
||||||
|
|
||||||
|
# Query trades from persistence layer
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
# First process current opened trades
|
||||||
|
for trade in trades:
|
||||||
|
state_changed |= self.process_maybe_execute_sell(trade)
|
||||||
|
|
||||||
|
# Then looking for buy opportunities
|
||||||
|
if len(trades) < self.config['max_open_trades']:
|
||||||
|
state_changed = self.process_maybe_execute_buy()
|
||||||
|
|
||||||
|
if 'unfilledtimeout' in self.config:
|
||||||
|
# Check and handle any timed out open orders
|
||||||
|
self.check_handle_timedout(self.config['unfilledtimeout'])
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||||
|
logger.warning('%s, retrying in 30 seconds...', error)
|
||||||
|
time.sleep(constants.RETRY_TIMEOUT)
|
||||||
|
except OperationalException:
|
||||||
|
self.rpc.send_msg(
|
||||||
|
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
|
||||||
|
.format(
|
||||||
|
traceback=traceback.format_exc(),
|
||||||
|
hint='Issue `/start` if you think it is safe to restart.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.exception('OperationalException. Stopping trader ...')
|
||||||
|
self.state = State.STOPPED
|
||||||
|
return state_changed
|
||||||
|
|
||||||
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
|
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
||||||
|
"""
|
||||||
|
Updates the whitelist with with a dynamically generated list
|
||||||
|
:param base_currency: base currency as str
|
||||||
|
:param key: sort key (defaults to 'BaseVolume')
|
||||||
|
:return: List of pairs
|
||||||
|
"""
|
||||||
|
summaries = sorted(
|
||||||
|
(s for s in exchange.get_market_summaries() if
|
||||||
|
s['MarketName'].startswith(base_currency)),
|
||||||
|
key=lambda s: s.get(key) or 0.0,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return [s['MarketName'].replace('-', '_') for s in summaries]
|
||||||
|
|
||||||
|
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
Check wallet health and remove pair from whitelist if necessary
|
||||||
|
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
|
||||||
|
trade
|
||||||
|
:return: the list of pairs the user wants to trade without the one unavailable or
|
||||||
|
black_listed
|
||||||
|
"""
|
||||||
|
sanitized_whitelist = whitelist
|
||||||
|
health = exchange.get_wallet_health()
|
||||||
|
known_pairs = set()
|
||||||
|
for status in health:
|
||||||
|
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
|
||||||
|
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
||||||
|
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
|
||||||
|
continue
|
||||||
|
# else the pair is valid
|
||||||
|
known_pairs.add(pair)
|
||||||
|
# Market is not active
|
||||||
|
if not status['IsActive']:
|
||||||
|
sanitized_whitelist.remove(pair)
|
||||||
|
logger.info(
|
||||||
|
'Ignoring %s from whitelist (reason: %s).',
|
||||||
|
pair, status.get('Notice') or 'wallet is not active'
|
||||||
|
)
|
||||||
|
|
||||||
|
# We need to remove pairs that are unknown
|
||||||
|
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
||||||
|
return final_list
|
||||||
|
|
||||||
|
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
||||||
|
"""
|
||||||
|
Calculates bid target between current ask price and last price
|
||||||
|
:param ticker: Ticker to use for getting Ask and Last Price
|
||||||
|
:return: float: Price
|
||||||
|
"""
|
||||||
|
if ticker['ask'] < ticker['last']:
|
||||||
|
return ticker['ask']
|
||||||
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
def create_trade(self) -> bool:
|
||||||
|
"""
|
||||||
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
|
:param stake_amount: amount of btc to spend
|
||||||
|
:param interval: Ticker interval used for Analyze
|
||||||
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
|
"""
|
||||||
|
stake_amount = self.config['stake_amount']
|
||||||
|
interval = self.analyze.get_ticker_interval()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
|
stake_amount
|
||||||
|
)
|
||||||
|
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
|
||||||
|
# Check if stake_amount is fulfilled
|
||||||
|
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
|
||||||
|
raise DependencyException(
|
||||||
|
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove currently opened and latest pairs from whitelist
|
||||||
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
|
if trade.pair in whitelist:
|
||||||
|
whitelist.remove(trade.pair)
|
||||||
|
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
|
|
||||||
|
if not whitelist:
|
||||||
|
raise DependencyException('No currency pairs in whitelist')
|
||||||
|
|
||||||
|
# Pick pair based on StochRSI buy signals
|
||||||
|
for _pair in whitelist:
|
||||||
|
(buy, sell) = self.analyze.get_signal(_pair, interval)
|
||||||
|
if buy and not sell:
|
||||||
|
pair = _pair
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Calculate amount
|
||||||
|
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
|
||||||
|
amount = stake_amount / buy_limit
|
||||||
|
|
||||||
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
|
|
||||||
|
stake_amount_fiat = self.fiat_converter.convert_amount(
|
||||||
|
stake_amount,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create trade entity and return
|
||||||
|
self.rpc.send_msg(
|
||||||
|
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
|
||||||
|
.format(
|
||||||
|
exchange.get_name().upper(),
|
||||||
|
pair.replace('_', '/'),
|
||||||
|
exchange.get_pair_detail_url(pair),
|
||||||
|
buy_limit,
|
||||||
|
stake_amount,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
stake_amount_fiat,
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
|
trade = Trade(
|
||||||
|
pair=pair,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=amount,
|
||||||
|
fee=exchange.get_fee(),
|
||||||
|
open_rate=buy_limit,
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
|
exchange=exchange.get_name().upper(),
|
||||||
|
open_order_id=order_id
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def process_maybe_execute_buy(self) -> bool:
|
||||||
|
"""
|
||||||
|
Tries to execute a buy trade in a safe way
|
||||||
|
:return: True if executed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Create entity and execute trade
|
||||||
|
if self.create_trade():
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
|
||||||
|
return False
|
||||||
|
except DependencyException as exception:
|
||||||
|
logger.warning('Unable to create trade: %s', exception)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_maybe_execute_sell(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Tries to execute a sell trade
|
||||||
|
:return: True if executed
|
||||||
|
"""
|
||||||
|
# Get order details for actual price per unit
|
||||||
|
if trade.open_order_id:
|
||||||
|
# Update trade with order values
|
||||||
|
logger.info('Found open order for %s', trade)
|
||||||
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
|
|
||||||
|
if trade.is_open and trade.open_order_id is None:
|
||||||
|
# Check if we can sell our current pair
|
||||||
|
return self.handle_trade(trade)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_trade(self, trade: Trade) -> bool:
|
||||||
|
"""
|
||||||
|
Sells the current pair if the threshold is reached and updates the trade record.
|
||||||
|
:return: True if trade has been sold, False otherwise
|
||||||
|
"""
|
||||||
|
if not trade.is_open:
|
||||||
|
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||||
|
|
||||||
|
logger.debug('Handling %s ...', trade)
|
||||||
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
|
(buy, sell) = (False, False)
|
||||||
|
|
||||||
|
if self.config.get('experimental', {}).get('use_sell_signal'):
|
||||||
|
(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):
|
||||||
|
self.execute_sell(trade, current_rate)
|
||||||
|
return True
|
||||||
|
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
||||||
|
"""
|
||||||
|
Check if any orders are timed out and cancel if neccessary
|
||||||
|
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
||||||
|
|
||||||
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
|
try:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
logger.info(
|
||||||
|
'Cannot query order for %s due to %s',
|
||||||
|
trade,
|
||||||
|
traceback.format_exc())
|
||||||
|
continue
|
||||||
|
ordertime = arrow.get(order['opened'])
|
||||||
|
|
||||||
|
# Check if trade is still actually open
|
||||||
|
if int(order['remaining']) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
|
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
|
||||||
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
|
# it is conditionally called in the
|
||||||
|
# handle_timedout_limit_sell()?
|
||||||
|
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
||||||
|
"""Buy timeout - cancel order
|
||||||
|
:return: True if order was fully cancelled
|
||||||
|
"""
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
if order['remaining'] == order['amount']:
|
||||||
|
# if trade is not partially completed, just delete the trade
|
||||||
|
Trade.session.delete(trade)
|
||||||
|
# FIX? do we really need to flush, caller of
|
||||||
|
# check_handle_timedout will flush afterwards
|
||||||
|
Trade.session.flush()
|
||||||
|
logger.info('Buy order timeout for %s.', trade)
|
||||||
|
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
return True
|
||||||
|
|
||||||
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
|
# and close the order
|
||||||
|
trade.amount = order['amount'] - order['remaining']
|
||||||
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
|
trade.open_order_id = None
|
||||||
|
logger.info('Partial buy order timeout for %s.', trade)
|
||||||
|
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||||
|
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Sell timeout - cancel order and update trade
|
||||||
|
:return: True if order was fully cancelled
|
||||||
|
"""
|
||||||
|
if order['remaining'] == order['amount']:
|
||||||
|
# if trade is not partially completed, just cancel the trade
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
trade.close_rate = None
|
||||||
|
trade.close_profit = None
|
||||||
|
trade.close_date = None
|
||||||
|
trade.is_open = True
|
||||||
|
trade.open_order_id = None
|
||||||
|
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
|
||||||
|
trade.pair.replace('_', '/')))
|
||||||
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_sell(self, trade: Trade, limit: float) -> None:
|
||||||
|
"""
|
||||||
|
Executes a limit sell for the given trade and limit
|
||||||
|
:param trade: Trade instance
|
||||||
|
:param limit: limit rate for the sell order
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# Execute sell and update trade record
|
||||||
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
||||||
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
|
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||||
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
profit = trade.calc_profit_percent(current_rate)
|
||||||
|
|
||||||
|
message = "*{exchange}:* Selling\n" \
|
||||||
|
"*Current Pair:* [{pair}]({pair_url})\n" \
|
||||||
|
"*Limit:* `{limit}`\n" \
|
||||||
|
"*Amount:* `{amount}`\n" \
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
|
"*Profit:* `{profit:.2f}%`" \
|
||||||
|
"".format(
|
||||||
|
exchange=trade.exchange,
|
||||||
|
pair=trade.pair,
|
||||||
|
pair_url=exchange.get_pair_detail_url(trade.pair),
|
||||||
|
limit=limit,
|
||||||
|
open_rate=trade.open_rate,
|
||||||
|
current_rate=current_rate,
|
||||||
|
amount=round(trade.amount, 8),
|
||||||
|
profit=round(profit * 100, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
# For regular case, when the configuration exists
|
||||||
|
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||||
|
fiat_converter = CryptoToFiatConverter()
|
||||||
|
profit_fiat = fiat_converter.convert_amount(
|
||||||
|
profit_trade,
|
||||||
|
self.config['stake_currency'],
|
||||||
|
self.config['fiat_display_currency']
|
||||||
|
)
|
||||||
|
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
||||||
|
'` / {profit_fiat:.3f} {fiat})`' \
|
||||||
|
''.format(
|
||||||
|
gain="profit" if fmt_exp_profit > 0 else "loss",
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade,
|
||||||
|
coin=self.config['stake_currency'],
|
||||||
|
profit_fiat=profit_fiat,
|
||||||
|
fiat=self.config['fiat_display_currency'],
|
||||||
|
)
|
||||||
|
# Because telegram._forcesell does not have the configuration
|
||||||
|
# Ignore the FIAT value and does not show the stake_currency as well
|
||||||
|
else:
|
||||||
|
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
||||||
|
gain="profit" if fmt_exp_profit > 0 else "loss",
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the message
|
||||||
|
self.rpc.send_msg(message)
|
||||||
|
Trade.session.flush()
|
40
freqtrade/indicator_helpers.py
Normal file
40
freqtrade/indicator_helpers.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from math import exp, pi, sqrt, cos
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import talib as ta
|
||||||
|
from pandas import Series
|
||||||
|
|
||||||
|
|
||||||
|
def went_up(series: Series) -> bool:
|
||||||
|
return series > series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
|
def went_down(series: Series) -> bool:
|
||||||
|
return series < series.shift(1)
|
||||||
|
|
||||||
|
|
||||||
|
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
|
||||||
|
magic = pi * sqrt(2) / smoothing
|
||||||
|
a1 = exp(-magic)
|
||||||
|
coeff2 = 2 * a1 * cos(magic)
|
||||||
|
coeff3 = -a1 * a1
|
||||||
|
coeff1 = (1 - coeff2 - coeff3) / 2
|
||||||
|
|
||||||
|
filtered = series.copy()
|
||||||
|
|
||||||
|
for i in range(2, len(series)):
|
||||||
|
filtered.iloc[i] = coeff1 * (series.iloc[i] + series.iloc[i-1]) + \
|
||||||
|
coeff2 * filtered.iloc[i-1] + coeff3 * filtered.iloc[i-2]
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
|
||||||
|
""" Does a smoothed fishers inverse transformation.
|
||||||
|
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
|
||||||
|
v1 = 0.1 * (series - 50)
|
||||||
|
if smoothing > 0:
|
||||||
|
v2 = ta.WMA(v1.values, timeperiod=smoothing)
|
||||||
|
else:
|
||||||
|
v2 = v1
|
||||||
|
return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)
|
@ -1,472 +1,69 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import copy
|
"""
|
||||||
import json
|
Main Freqtrade bot script.
|
||||||
|
Read the documentation to know what cli arguments you need.
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
from typing import List
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
import arrow
|
from freqtrade.arguments import Arguments
|
||||||
import requests
|
from freqtrade.configuration import Configuration
|
||||||
from cachetools import cached, TTLCache
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException, __version__,
|
|
||||||
exchange, persistence, rpc)
|
|
||||||
from freqtrade.analyze import SignalType, get_signal
|
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.misc import (State, get_state, load_config, parse_args,
|
|
||||||
throttle, update_state)
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
_CONF = {}
|
|
||||||
|
|
||||||
|
def main(sysargv: List[str]) -> None:
|
||||||
def refresh_whitelist(whitelist: List[str]) -> List[str]:
|
|
||||||
"""
|
"""
|
||||||
Check wallet health and remove pair from whitelist if necessary
|
This function will initiate the bot and start the trading loop.
|
||||||
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to trade
|
|
||||||
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
|
|
||||||
"""
|
|
||||||
sanitized_whitelist = whitelist
|
|
||||||
health = exchange.get_wallet_health()
|
|
||||||
known_pairs = set()
|
|
||||||
for status in health:
|
|
||||||
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
|
|
||||||
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
|
|
||||||
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
|
|
||||||
continue
|
|
||||||
# else the pair is valid
|
|
||||||
known_pairs.add(pair)
|
|
||||||
# Market is not active
|
|
||||||
if not status['IsActive']:
|
|
||||||
sanitized_whitelist.remove(pair)
|
|
||||||
logger.info(
|
|
||||||
'Ignoring %s from whitelist (reason: %s).',
|
|
||||||
pair, status.get('Notice') or 'wallet is not active'
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to remove pairs that are unknown
|
|
||||||
final_list = [x for x in sanitized_whitelist if x in known_pairs]
|
|
||||||
return final_list
|
|
||||||
|
|
||||||
|
|
||||||
def _process(nb_assets: Optional[int] = 0) -> bool:
|
|
||||||
"""
|
|
||||||
Queries the persistence layer for open trades and handles them,
|
|
||||||
otherwise a new trade is created.
|
|
||||||
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
|
||||||
:return: True if a trade has been created or closed, False otherwise
|
|
||||||
"""
|
|
||||||
state_changed = False
|
|
||||||
try:
|
|
||||||
# Refresh whitelist based on wallet maintenance
|
|
||||||
sanitized_list = refresh_whitelist(
|
|
||||||
gen_pair_whitelist(
|
|
||||||
_CONF['stake_currency']
|
|
||||||
) if nb_assets else _CONF['exchange']['pair_whitelist']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep only the subsets of pairs wanted (up to nb_assets)
|
|
||||||
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
|
||||||
_CONF['exchange']['pair_whitelist'] = final_list
|
|
||||||
|
|
||||||
# Query trades from persistence layer
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
if len(trades) < _CONF['max_open_trades']:
|
|
||||||
try:
|
|
||||||
# Create entity and execute trade
|
|
||||||
state_changed = create_trade(float(_CONF['stake_amount']))
|
|
||||||
if not state_changed:
|
|
||||||
logger.info(
|
|
||||||
'Checked all whitelisted currencies. '
|
|
||||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
|
||||||
)
|
|
||||||
except DependencyException as exception:
|
|
||||||
logger.warning('Unable to create trade: %s', exception)
|
|
||||||
|
|
||||||
for trade in trades:
|
|
||||||
# Get order details for actual price per unit
|
|
||||||
if trade.open_order_id:
|
|
||||||
# Update trade with order values
|
|
||||||
logger.info('Got open order for %s', trade)
|
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
|
||||||
|
|
||||||
if trade.is_open and trade.open_order_id is None:
|
|
||||||
# Check if we can sell our current pair
|
|
||||||
state_changed = handle_trade(trade) or state_changed
|
|
||||||
|
|
||||||
if 'unfilledtimeout' in _CONF:
|
|
||||||
# Check and handle any timed out open orders
|
|
||||||
check_handle_timedout(_CONF['unfilledtimeout'])
|
|
||||||
|
|
||||||
Trade.session.flush()
|
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
|
||||||
logger.warning(
|
|
||||||
'Got %s in _process(), retrying in 30 seconds...',
|
|
||||||
error
|
|
||||||
)
|
|
||||||
time.sleep(30)
|
|
||||||
except OperationalException:
|
|
||||||
rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
|
|
||||||
traceback=traceback.format_exc(),
|
|
||||||
hint='Issue `/start` if you think it is safe to restart.'
|
|
||||||
))
|
|
||||||
logger.exception('Got OperationalException. Stopping trader ...')
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
return state_changed
|
|
||||||
|
|
||||||
|
|
||||||
def check_handle_timedout(timeoutvalue: int) -> None:
|
|
||||||
"""
|
|
||||||
Check if any orders are timed out and cancel if neccessary
|
|
||||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
arguments = Arguments(
|
||||||
|
sysargv,
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
'Simple High Frequency Trading Bot for crypto currencies'
|
||||||
order = exchange.get_order(trade.open_order_id)
|
|
||||||
ordertime = arrow.get(order['opened'])
|
|
||||||
|
|
||||||
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
|
|
||||||
# Buy timeout - cancel order
|
|
||||||
exchange.cancel_order(trade.open_order_id)
|
|
||||||
if order['remaining'] == order['amount']:
|
|
||||||
# if trade is not partially completed, just delete the trade
|
|
||||||
Trade.session.delete(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
|
||||||
else:
|
|
||||||
# if trade is partially complete, edit the stake details for the trade
|
|
||||||
# and close the order
|
|
||||||
trade.amount = order['amount'] - order['remaining']
|
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
|
||||||
trade.open_order_id = None
|
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
|
||||||
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
|
|
||||||
# Sell timeout - cancel order and update trade
|
|
||||||
if order['remaining'] == order['amount']:
|
|
||||||
# if trade is not partially completed, just cancel the trade
|
|
||||||
exchange.cancel_order(trade.open_order_id)
|
|
||||||
trade.close_rate = None
|
|
||||||
trade.close_profit = None
|
|
||||||
trade.close_date = None
|
|
||||||
trade.is_open = True
|
|
||||||
trade.open_order_id = None
|
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, limit: float) -> None:
|
|
||||||
"""
|
|
||||||
Executes a limit sell for the given trade and limit
|
|
||||||
:param trade: Trade instance
|
|
||||||
:param limit: limit rate for the sell order
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Execute sell and update trade record
|
|
||||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
|
||||||
trade.open_order_id = order_id
|
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
|
||||||
profit_trade = trade.calc_profit(rate=limit)
|
|
||||||
|
|
||||||
message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
|
|
||||||
exchange=trade.exchange,
|
|
||||||
pair=trade.pair.replace('_', '/'),
|
|
||||||
pair_url=exchange.get_pair_detail_url(trade.pair),
|
|
||||||
limit=limit
|
|
||||||
)
|
)
|
||||||
|
args = arguments.get_parsed_arg()
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# A subcommand has been issued.
|
||||||
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
|
# Means if Backtesting or Hyperopt have been called we exit the bot
|
||||||
fiat_converter = CryptoToFiatConverter()
|
|
||||||
profit_fiat = fiat_converter.convert_amount(
|
|
||||||
profit_trade,
|
|
||||||
_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency']
|
|
||||||
)
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
|
||||||
'` / {profit_fiat:.3f} {fiat})`'.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade,
|
|
||||||
coin=_CONF['stake_currency'],
|
|
||||||
profit_fiat=profit_fiat,
|
|
||||||
fiat=_CONF['fiat_display_currency'],
|
|
||||||
)
|
|
||||||
# Because telegram._forcesell does not have the configuration
|
|
||||||
# Ignore the FIAT value and does not show the stake_currency as well
|
|
||||||
else:
|
|
||||||
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
|
||||||
gain="profit" if fmt_exp_profit > 0 else "loss",
|
|
||||||
profit_percent=fmt_exp_profit,
|
|
||||||
profit_coin=profit_trade
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send the message
|
|
||||||
rpc.send_msg(message)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
|
||||||
"""
|
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
|
||||||
:return True if bot should sell at current rate
|
|
||||||
"""
|
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
|
||||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
|
||||||
logger.debug('Stop loss hit.')
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Check if time matches and current rate is above threshold
|
|
||||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
|
||||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
|
||||||
if time_diff > float(duration) and current_profit > threshold:
|
|
||||||
return True
|
|
||||||
|
|
||||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def handle_trade(trade: Trade) -> bool:
|
|
||||||
"""
|
|
||||||
Sells the current pair if the threshold is reached and updates the trade record.
|
|
||||||
:return: True if trade has been sold, False otherwise
|
|
||||||
"""
|
|
||||||
if not trade.is_open:
|
|
||||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
|
||||||
|
|
||||||
# Check if minimal roi has been reached
|
|
||||||
if min_roi_reached(trade, current_rate, datetime.utcnow()):
|
|
||||||
logger.debug('Executing sell due to ROI ...')
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Experimental: Check if sell signal has been enabled and triggered
|
|
||||||
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
|
||||||
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
|
|
||||||
if _CONF.get('experimental', {}).get('sell_profit_only'):
|
|
||||||
logger.debug('Checking if trade is profitable ...')
|
|
||||||
if trade.calc_profit(rate=current_rate) <= 0:
|
|
||||||
return False
|
|
||||||
logger.debug('Checking sell_signal ...')
|
|
||||||
if get_signal(trade.pair, SignalType.SELL):
|
|
||||||
logger.debug('Executing sell due to sell signal ...')
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_target_bid(ticker: Dict[str, float]) -> float:
|
|
||||||
""" Calculates bid target between current ask price and last price """
|
|
||||||
if ticker['ask'] < ticker['last']:
|
|
||||||
return ticker['ask']
|
|
||||||
balance = _CONF['bid_strategy']['ask_last_balance']
|
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float) -> bool:
|
|
||||||
"""
|
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
|
||||||
:param stake_amount: amount of btc to spend
|
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
|
||||||
stake_amount
|
|
||||||
)
|
|
||||||
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
|
||||||
# Check if stake_amount is fulfilled
|
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
|
||||||
raise DependencyException(
|
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
|
||||||
if trade.pair in whitelist:
|
|
||||||
whitelist.remove(trade.pair)
|
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
|
||||||
if not whitelist:
|
|
||||||
raise DependencyException('No pair in whitelist')
|
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
|
||||||
for _pair in whitelist:
|
|
||||||
if get_signal(_pair, SignalType.BUY):
|
|
||||||
pair = _pair
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Calculate amount
|
|
||||||
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
|
||||||
amount = stake_amount / buy_limit
|
|
||||||
|
|
||||||
order_id = exchange.buy(pair, buy_limit, amount)
|
|
||||||
|
|
||||||
fiat_converter = CryptoToFiatConverter()
|
|
||||||
stake_amount_fiat = fiat_converter.convert_amount(
|
|
||||||
stake_amount,
|
|
||||||
_CONF['stake_currency'],
|
|
||||||
_CONF['fiat_display_currency']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create trade entity and return
|
|
||||||
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
|
|
||||||
exchange.get_name().upper(),
|
|
||||||
pair.replace('_', '/'),
|
|
||||||
exchange.get_pair_detail_url(pair),
|
|
||||||
buy_limit, stake_amount, _CONF['stake_currency'],
|
|
||||||
stake_amount_fiat, _CONF['fiat_display_currency']
|
|
||||||
))
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
|
||||||
trade = Trade(
|
|
||||||
pair=pair,
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=amount,
|
|
||||||
fee=exchange.get_fee(),
|
|
||||||
open_rate=buy_limit,
|
|
||||||
open_date=datetime.utcnow(),
|
|
||||||
exchange=exchange.get_name().upper(),
|
|
||||||
open_order_id=order_id
|
|
||||||
)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
|
||||||
"""
|
|
||||||
Initializes all modules and updates the config
|
|
||||||
:param config: config as dict
|
|
||||||
:param db_url: database connector string for sqlalchemy (Optional)
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Initialize all modules
|
|
||||||
rpc.init(config)
|
|
||||||
persistence.init(config, db_url)
|
|
||||||
exchange.init(config)
|
|
||||||
|
|
||||||
# Set initial application state
|
|
||||||
initial_state = config.get('initial_state')
|
|
||||||
if initial_state:
|
|
||||||
update_state(State[initial_state.upper()])
|
|
||||||
else:
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
|
||||||
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
|
||||||
"""
|
|
||||||
Updates the whitelist with with a dynamically generated list
|
|
||||||
:param base_currency: base currency as str
|
|
||||||
:param key: sort key (defaults to 'BaseVolume')
|
|
||||||
:return: List of pairs
|
|
||||||
"""
|
|
||||||
summaries = sorted(
|
|
||||||
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
|
|
||||||
key=lambda s: s.get(key) or 0.0,
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return [s['MarketName'].replace('-', '_') for s in summaries]
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
|
||||||
"""
|
|
||||||
Cleanup the application state und finish all pending tasks
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
rpc.send_msg('*Status:* `Stopping trader...`')
|
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
persistence.cleanup()
|
|
||||||
rpc.cleanup()
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def main(sysargv=sys.argv[1:]) -> None:
|
|
||||||
"""
|
|
||||||
Loads and validates the config and handles the main loop
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
global _CONF
|
|
||||||
args = parse_args(sysargv,
|
|
||||||
'Simple High Frequency Trading Bot for crypto currencies')
|
|
||||||
|
|
||||||
# A subcommand has been issued
|
|
||||||
if hasattr(args, 'func'):
|
if hasattr(args, 'func'):
|
||||||
args.func(args)
|
args.func(args)
|
||||||
exit(0)
|
return
|
||||||
|
|
||||||
# Initialize logger
|
|
||||||
logging.basicConfig(
|
|
||||||
level=args.loglevel,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'Starting freqtrade %s (loglevel=%s)',
|
|
||||||
__version__,
|
|
||||||
logging.getLevelName(args.loglevel)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load and validate configuration
|
|
||||||
_CONF = load_config(args.config)
|
|
||||||
|
|
||||||
# Initialize all modules and start main loop
|
|
||||||
if args.dynamic_whitelist:
|
|
||||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
|
||||||
|
|
||||||
# If the user ask for Dry run with a local DB instead of memory
|
|
||||||
if args.dry_run_db:
|
|
||||||
if _CONF.get('dry_run', False):
|
|
||||||
_CONF.update({'dry_run_db': True})
|
|
||||||
logger.info(
|
|
||||||
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
|
||||||
|
|
||||||
|
freqtrade = None
|
||||||
|
return_code = 1
|
||||||
try:
|
try:
|
||||||
init(_CONF)
|
# Load and validate configuration
|
||||||
old_state = None
|
config = Configuration(args).get_config()
|
||||||
while True:
|
|
||||||
new_state = get_state()
|
# Init the bot
|
||||||
# Log state transition
|
freqtrade = FreqtradeBot(config)
|
||||||
if new_state != old_state:
|
|
||||||
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
state = None
|
||||||
logger.info('Changing state to: %s', new_state.name)
|
while 1:
|
||||||
|
state = freqtrade.worker(old_state=state)
|
||||||
|
|
||||||
if new_state == State.STOPPED:
|
|
||||||
time.sleep(1)
|
|
||||||
elif new_state == State.RUNNING:
|
|
||||||
throttle(
|
|
||||||
_process,
|
|
||||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
|
||||||
nb_assets=args.dynamic_whitelist,
|
|
||||||
)
|
|
||||||
old_state = new_state
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.info('Got SIGINT, aborting ...')
|
logger.info('SIGINT received, aborting ...')
|
||||||
|
return_code = 0
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.exception('Got fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
cleanup()
|
if freqtrade:
|
||||||
|
freqtrade.clean()
|
||||||
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
|
def set_loggers() -> None:
|
||||||
|
"""
|
||||||
|
Set the logger level for Third party libs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
set_loggers()
|
||||||
|
main(sys.argv[1:])
|
||||||
|
@ -1,318 +1,74 @@
|
|||||||
import argparse
|
"""
|
||||||
import enum
|
Various tool function for Freqtrade and scripts
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import re
|
||||||
import os
|
from datetime import datetime
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Dict
|
||||||
|
|
||||||
from jsonschema import Draft4Validator, validate
|
import numpy as np
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from pandas import DataFrame
|
||||||
from wrapt import synchronized
|
|
||||||
|
|
||||||
from freqtrade import __version__
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class State(enum.Enum):
|
def shorten_date(_date: str) -> str:
|
||||||
RUNNING = 0
|
|
||||||
STOPPED = 1
|
|
||||||
|
|
||||||
|
|
||||||
# Current application state
|
|
||||||
_STATE = State.STOPPED
|
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
|
||||||
def update_state(state: State) -> None:
|
|
||||||
"""
|
"""
|
||||||
Updates the application state
|
Trim the date so it fits on small screens
|
||||||
:param state: new state
|
|
||||||
:return: None
|
|
||||||
"""
|
"""
|
||||||
global _STATE
|
new_date = re.sub('seconds?', 'sec', _date)
|
||||||
_STATE = state
|
new_date = re.sub('minutes?', 'min', new_date)
|
||||||
|
new_date = re.sub('hours?', 'h', new_date)
|
||||||
|
new_date = re.sub('days?', 'd', new_date)
|
||||||
|
new_date = re.sub('^an?', '1', new_date)
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
@synchronized
|
############################################
|
||||||
def get_state() -> State:
|
# Used by scripts #
|
||||||
|
# Matplotlib doesn't support ::datetime64, #
|
||||||
|
# so we need to convert it into ::datetime #
|
||||||
|
############################################
|
||||||
|
def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Gets the current application state
|
Convert an pandas-array of timestamps into
|
||||||
|
An numpy-array of datetimes
|
||||||
|
:return: numpy-array of datetime
|
||||||
|
"""
|
||||||
|
times = []
|
||||||
|
dates = dates.astype(datetime)
|
||||||
|
for index in range(0, dates.size):
|
||||||
|
date = dates[index].to_pydatetime()
|
||||||
|
times.append(date)
|
||||||
|
return np.array(times)
|
||||||
|
|
||||||
|
|
||||||
|
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Return dates from Dataframe
|
||||||
|
:param dfs: Dict with format pair: pair_data
|
||||||
|
:return: List of dates
|
||||||
|
"""
|
||||||
|
alldates = {}
|
||||||
|
for pair, pair_data in dfs.items():
|
||||||
|
dates = datesarray_to_datetimearray(pair_data['date'])
|
||||||
|
for date in dates:
|
||||||
|
alldates[date] = 1
|
||||||
|
lst = []
|
||||||
|
for date, _ in alldates.items():
|
||||||
|
lst.append(date)
|
||||||
|
arr = np.array(lst)
|
||||||
|
return np.sort(arr, axis=0)
|
||||||
|
|
||||||
|
|
||||||
|
def file_dump_json(filename, data) -> None:
|
||||||
|
"""
|
||||||
|
Dump JSON data into a file
|
||||||
|
:param filename: file to create
|
||||||
|
:param data: JSON Data to save
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return _STATE
|
with open(filename, 'w') as fp:
|
||||||
|
json.dump(data, fp, default=str)
|
||||||
|
|
||||||
def load_config(path: str) -> Dict:
|
|
||||||
"""
|
|
||||||
Loads a config file from the given path
|
|
||||||
:param path: path as str
|
|
||||||
:return: configuration as dictionary
|
|
||||||
"""
|
|
||||||
with open(path) as file:
|
|
||||||
conf = json.load(file)
|
|
||||||
if 'internals' not in conf:
|
|
||||||
conf['internals'] = {}
|
|
||||||
logger.info('Validating configuration ...')
|
|
||||||
try:
|
|
||||||
validate(conf, CONF_SCHEMA)
|
|
||||||
return conf
|
|
||||||
except ValidationError as exception:
|
|
||||||
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
|
|
||||||
raise ValidationError(
|
|
||||||
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
|
||||||
"""
|
|
||||||
Throttles the given callable that it
|
|
||||||
takes at least `min_secs` to finish execution.
|
|
||||||
:param func: Any callable
|
|
||||||
:param min_secs: minimum execution time in seconds
|
|
||||||
:return: Any
|
|
||||||
"""
|
|
||||||
start = time.time()
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
end = time.time()
|
|
||||||
duration = max(min_secs - (end - start), 0.0)
|
|
||||||
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
|
|
||||||
time.sleep(duration)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def common_args_parser(description: str):
|
|
||||||
"""
|
|
||||||
Parses given common arguments and returns them as a parsed object.
|
|
||||||
"""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-v', '--verbose',
|
|
||||||
help='be verbose',
|
|
||||||
action='store_const',
|
|
||||||
dest='loglevel',
|
|
||||||
const=logging.DEBUG,
|
|
||||||
default=logging.INFO,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--version',
|
|
||||||
action='version',
|
|
||||||
version='%(prog)s {}'.format(__version__),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-c', '--config',
|
|
||||||
help='specify configuration file (default: config.json)',
|
|
||||||
dest='config',
|
|
||||||
default='config.json',
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args: List[str], description: str):
|
|
||||||
"""
|
|
||||||
Parses given arguments and returns an argparse Namespace instance.
|
|
||||||
Returns None if a sub command has been selected and executed.
|
|
||||||
"""
|
|
||||||
parser = common_args_parser(description)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run-db',
|
|
||||||
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
|
|
||||||
instead of memory DB. Work only if dry_run is enabled.',
|
|
||||||
action='store_true',
|
|
||||||
dest='dry_run_db',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-dd', '--datadir',
|
|
||||||
help='path to backtest data (default freqdata/tests/testdata',
|
|
||||||
dest='datadir',
|
|
||||||
default=os.path.join('freqtrade', 'tests', 'testdata'),
|
|
||||||
type=str,
|
|
||||||
metavar='PATH',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dynamic-whitelist',
|
|
||||||
help='dynamically generate and update whitelist \
|
|
||||||
based on 24h BaseVolume (Default 20 currencies)', # noqa
|
|
||||||
dest='dynamic_whitelist',
|
|
||||||
const=20,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
nargs='?',
|
|
||||||
)
|
|
||||||
|
|
||||||
build_subcommands(parser)
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|
||||||
""" Builds and attaches all subcommands """
|
|
||||||
from freqtrade.optimize import backtesting, hyperopt
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='subparser')
|
|
||||||
|
|
||||||
# Add backtesting subcommand
|
|
||||||
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
|
||||||
backtesting_cmd.set_defaults(func=backtesting.start)
|
|
||||||
backtesting_cmd.add_argument(
|
|
||||||
'-l', '--live',
|
|
||||||
action='store_true',
|
|
||||||
dest='live',
|
|
||||||
help='using live data',
|
|
||||||
)
|
|
||||||
backtesting_cmd.add_argument(
|
|
||||||
'-i', '--ticker-interval',
|
|
||||||
help='specify ticker interval in minutes (default: 5)',
|
|
||||||
dest='ticker_interval',
|
|
||||||
default=5,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
)
|
|
||||||
backtesting_cmd.add_argument(
|
|
||||||
'--realistic-simulation',
|
|
||||||
help='uses max_open_trades from config to simulate real world limitations',
|
|
||||||
action='store_true',
|
|
||||||
dest='realistic_simulation',
|
|
||||||
)
|
|
||||||
backtesting_cmd.add_argument(
|
|
||||||
'-r', '--refresh-pairs-cached',
|
|
||||||
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
|
||||||
Use it if you want to run your backtesting with up-to-date data.',
|
|
||||||
action='store_true',
|
|
||||||
dest='refresh_pairs',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
|
||||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
|
||||||
hyperopt_cmd.add_argument(
|
|
||||||
'-e', '--epochs',
|
|
||||||
help='specify number of epochs (default: 100)',
|
|
||||||
dest='epochs',
|
|
||||||
default=100,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
)
|
|
||||||
hyperopt_cmd.add_argument(
|
|
||||||
'--use-mongodb',
|
|
||||||
help='parallelize evaluations with mongodb (requires mongod in PATH)',
|
|
||||||
dest='mongodb',
|
|
||||||
action='store_true',
|
|
||||||
)
|
|
||||||
hyperopt_cmd.add_argument(
|
|
||||||
'-i', '--ticker-interval',
|
|
||||||
help='specify ticker interval in minutes (default: 5)',
|
|
||||||
dest='ticker_interval',
|
|
||||||
default=5,
|
|
||||||
type=int,
|
|
||||||
metavar='INT',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
|
||||||
CONF_SCHEMA = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
|
||||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': ['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']},
|
|
||||||
'dry_run': {'type': 'boolean'},
|
|
||||||
'minimal_roi': {
|
|
||||||
'type': 'object',
|
|
||||||
'patternProperties': {
|
|
||||||
'^[0-9.]+$': {'type': 'number'}
|
|
||||||
},
|
|
||||||
'minProperties': 1
|
|
||||||
},
|
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
|
||||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
|
||||||
'bid_strategy': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'ask_last_balance': {
|
|
||||||
'type': 'number',
|
|
||||||
'minimum': 0,
|
|
||||||
'maximum': 1,
|
|
||||||
'exclusiveMaximum': False
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'required': ['ask_last_balance']
|
|
||||||
},
|
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
|
||||||
'experimental': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
|
||||||
'sell_profit_only': {'type': 'boolean'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'telegram': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'enabled': {'type': 'boolean'},
|
|
||||||
'token': {'type': 'string'},
|
|
||||||
'chat_id': {'type': 'string'},
|
|
||||||
},
|
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
|
||||||
},
|
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
|
||||||
'internals': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'process_throttle_secs': {'type': 'number'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'definitions': {
|
|
||||||
'exchange': {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'name': {'type': 'string'},
|
|
||||||
'key': {'type': 'string'},
|
|
||||||
'secret': {'type': 'string'},
|
|
||||||
'pair_whitelist': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'type': 'string',
|
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
|
||||||
},
|
|
||||||
'uniqueItems': True
|
|
||||||
},
|
|
||||||
'pair_blacklist': {
|
|
||||||
'type': 'array',
|
|
||||||
'items': {
|
|
||||||
'type': 'string',
|
|
||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
|
||||||
},
|
|
||||||
'uniqueItems': True
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'anyOf': [
|
|
||||||
{'required': ['exchange']}
|
|
||||||
],
|
|
||||||
'required': [
|
|
||||||
'max_open_trades',
|
|
||||||
'stake_currency',
|
|
||||||
'stake_amount',
|
|
||||||
'fiat_display_currency',
|
|
||||||
'dry_run',
|
|
||||||
'minimal_roi',
|
|
||||||
'bid_strategy',
|
|
||||||
'telegram'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
@ -1,44 +1,69 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
import logging
|
import gzip
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict, Tuple
|
||||||
from pandas import DataFrame
|
|
||||||
|
from freqtrade import misc
|
||||||
from freqtrade.exchange import get_ticker_history
|
from freqtrade.exchange import get_ticker_history
|
||||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||||
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_tickerdata_file(datadir, pair, ticker_interval):
|
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
|
||||||
|
stype, start, stop = timerange
|
||||||
|
if stype == (None, 'line'):
|
||||||
|
return tickerlist[stop:]
|
||||||
|
elif stype == ('line', None):
|
||||||
|
return tickerlist[0:start]
|
||||||
|
elif stype == ('index', 'index'):
|
||||||
|
return tickerlist[start:stop]
|
||||||
|
|
||||||
|
return tickerlist
|
||||||
|
|
||||||
|
|
||||||
|
def load_tickerdata_file(
|
||||||
|
datadir: str, pair: str,
|
||||||
|
ticker_interval: int,
|
||||||
|
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
|
||||||
"""
|
"""
|
||||||
Load a pair from file,
|
Load a pair from file,
|
||||||
:return dict OR empty if unsuccesful
|
:return dict OR empty if unsuccesful
|
||||||
"""
|
"""
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
file = '{abspath}/{pair}-{ticker_interval}.json'.format(
|
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
|
||||||
abspath=path,
|
|
||||||
pair=pair,
|
pair=pair,
|
||||||
ticker_interval=ticker_interval,
|
ticker_interval=ticker_interval,
|
||||||
)
|
))
|
||||||
# The file does not exist we download it
|
gzipfile = file + '.gz'
|
||||||
if not os.path.isfile(file):
|
|
||||||
|
# If the file does not exist we download it when None is returned.
|
||||||
|
# If file exists, read the file, load the json
|
||||||
|
if os.path.isfile(gzipfile):
|
||||||
|
logger.debug('Loading ticker data from file %s', gzipfile)
|
||||||
|
with gzip.open(gzipfile) as tickerdata:
|
||||||
|
pairdata = json.load(tickerdata)
|
||||||
|
elif os.path.isfile(file):
|
||||||
|
logger.debug('Loading ticker data from file %s', file)
|
||||||
|
with open(file) as tickerdata:
|
||||||
|
pairdata = json.load(tickerdata)
|
||||||
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Read the file, load the json
|
if timerange:
|
||||||
with open(file) as tickerdata:
|
pairdata = trim_tickerlist(pairdata, timerange)
|
||||||
pairdata = json.load(tickerdata)
|
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None,
|
def load_data(datadir: str, ticker_interval: int,
|
||||||
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
|
pairs: Optional[List[str]] = None,
|
||||||
|
refresh_pairs: Optional[bool] = False,
|
||||||
|
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Loads ticker history data for the given parameters
|
||||||
:param ticker_interval: ticker interval in minutes
|
|
||||||
:param pairs: list of pairs
|
|
||||||
:return: dict
|
:return: dict
|
||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
@ -48,86 +73,76 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]]
|
|||||||
# 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)
|
download_pairs(datadir, _pairs, ticker_interval)
|
||||||
|
|
||||||
for pair in _pairs:
|
for pair in _pairs:
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
if not pairdata:
|
if not pairdata:
|
||||||
# download the tickerdata from exchange
|
# download the tickerdata from exchange
|
||||||
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
||||||
# and retry reading the pair
|
# and retry reading the pair
|
||||||
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
|
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
|
||||||
result[pair] = pairdata
|
result[pair] = pairdata
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
|
||||||
"""Creates a dataframe and populates indicators for given ticker data"""
|
|
||||||
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
|
|
||||||
for pair, pair_data in tickerdata.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def make_testdata_path(datadir: str) -> str:
|
def make_testdata_path(datadir: str) -> str:
|
||||||
"""Return the path where testdata files are stored"""
|
"""Return the path where testdata files are stored"""
|
||||||
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__),
|
return datadir or os.path.abspath(
|
||||||
'..', 'tests', 'testdata'))
|
os.path.join(
|
||||||
|
os.path.dirname(__file__), '..', 'tests', 'testdata'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_pairs(datadir, pairs: List[str]) -> bool:
|
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
|
||||||
"""For each pairs passed in parameters, download 1 and 5 ticker intervals"""
|
"""For each pairs passed in parameters, download the ticker intervals"""
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
try:
|
try:
|
||||||
for interval in [1, 5]:
|
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
|
||||||
download_backtesting_testdata(datadir, pair=pair, interval=interval)
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
|
logger.info(
|
||||||
pair=pair,
|
'Failed to download the pair: "%s", Interval: %s min',
|
||||||
interval=interval,
|
pair,
|
||||||
))
|
ticker_interval
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
|
# FIX: 20180110, suggest rename interval to tick_interval
|
||||||
|
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> None:
|
||||||
"""
|
"""
|
||||||
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
|
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
|
||||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||||
:param pairs: list of pairs to download
|
|
||||||
:return: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format(
|
logger.info(
|
||||||
pair=pair,
|
'Download the pair: "%s", Interval: %s min', pair, interval
|
||||||
interval=interval,
|
)
|
||||||
))
|
|
||||||
|
|
||||||
filepair = pair.replace("-", "_")
|
|
||||||
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
||||||
pair=filepair,
|
pair=pair.replace("-", "_"),
|
||||||
interval=interval,
|
interval=interval,
|
||||||
))
|
))
|
||||||
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename, "rt") as fp:
|
with open(filename, "rt") as file:
|
||||||
data = json.load(fp)
|
data = json.load(file)
|
||||||
logger.debug("Current Start: {}".format(data[1]['T']))
|
|
||||||
logger.debug("Current End: {}".format(data[-1:][0]['T']))
|
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
logger.debug("Current Start: None")
|
|
||||||
logger.debug("Current End: None")
|
|
||||||
|
|
||||||
new_data = get_ticker_history(pair=pair, tick_interval=int(interval))
|
logger.debug('Current Start: %s', data[1]['T'] if data else None)
|
||||||
for row in new_data:
|
logger.debug('Current End: %s', data[-1:][0]['T'] if data else None)
|
||||||
if row not in data:
|
|
||||||
data.append(row)
|
|
||||||
logger.debug("New Start: {}".format(data[1]['T']))
|
|
||||||
logger.debug("New End: {}".format(data[-1:][0]['T']))
|
|
||||||
data = sorted(data, key=lambda data: data['T'])
|
|
||||||
|
|
||||||
with open(filename, "wt") as fp:
|
# Extend data with new ticker history
|
||||||
json.dump(data, fp)
|
data.extend([
|
||||||
|
row for row in get_ticker_history(pair=pair, tick_interval=int(interval))
|
||||||
|
if row not in data
|
||||||
|
])
|
||||||
|
|
||||||
return True
|
data = sorted(data, key=lambda _data: _data['T'])
|
||||||
|
logger.debug('New Start: %s', data[1]['T'])
|
||||||
|
logger.debug('New End: %s', data[-1:][0]['T'])
|
||||||
|
misc.file_dump_json(filename, data)
|
||||||
|
@ -1,197 +1,307 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains the backtesting logic
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Tuple
|
import operator
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Dict, Tuple, Any, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame, Series
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
import freqtrade.misc as misc
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import min_roi_reached
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.optimize import preprocess
|
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
class Backtesting(object):
|
||||||
"""
|
"""
|
||||||
Get the maximum timeframe for the given backtest data
|
Backtesting class, this class contains all the logic to run a backtest
|
||||||
:param data: dictionary with preprocessed backtesting data
|
|
||||||
:return: tuple containing min_date, max_date
|
|
||||||
"""
|
|
||||||
all_dates = Series([])
|
|
||||||
for pair, pair_data in data.items():
|
|
||||||
all_dates = all_dates.append(pair_data['date'])
|
|
||||||
all_dates.sort_values(inplace=True)
|
|
||||||
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
|
|
||||||
|
|
||||||
|
To run a backtest:
|
||||||
|
backtesting = Backtesting(config)
|
||||||
|
backtesting.start()
|
||||||
|
"""
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
self.config = config
|
||||||
|
self.analyze = None
|
||||||
|
self.ticker_interval = None
|
||||||
|
self.tickerdata_to_dataframe = None
|
||||||
|
self.populate_buy_trend = None
|
||||||
|
self.populate_sell_trend = None
|
||||||
|
self._init()
|
||||||
|
|
||||||
def generate_text_table(
|
def _init(self) -> None:
|
||||||
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str:
|
"""
|
||||||
"""
|
Init objects required for backtesting
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
:return: None
|
||||||
:return: pretty printed table with tabulate as str
|
"""
|
||||||
"""
|
self.analyze = Analyze(self.config)
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
self.ticker_interval = self.analyze.strategy.ticker_interval
|
||||||
tabular_data = []
|
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
||||||
headers = ['pair', 'buy count', 'avg profit %',
|
self.populate_buy_trend = self.analyze.populate_buy_trend
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
self.populate_sell_trend = self.analyze.populate_sell_trend
|
||||||
for pair in data:
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
result = results[results.currency == pair]
|
|
||||||
|
@staticmethod
|
||||||
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with preprocessed backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
timeframe = [
|
||||||
|
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
|
||||||
|
for frame in data.values()
|
||||||
|
]
|
||||||
|
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||||
|
max(timeframe, key=operator.itemgetter(1))[1]
|
||||||
|
|
||||||
|
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:return: pretty printed table with tabulate as str
|
||||||
|
"""
|
||||||
|
stake_currency = self.config.get('stake_currency')
|
||||||
|
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit %',
|
||||||
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.currency == pair]
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
result.profit_percent.mean() * 100.0,
|
||||||
|
result.profit_BTC.sum(),
|
||||||
|
result.duration.mean(),
|
||||||
|
len(result[result.profit_BTC > 0]),
|
||||||
|
len(result[result.profit_BTC < 0])
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
'TOTAL',
|
||||||
len(result.index),
|
len(results.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
result.profit_BTC.sum(),
|
results.profit_BTC.sum(),
|
||||||
result.duration.mean() * ticker_interval,
|
results.duration.mean(),
|
||||||
result.profit.sum(),
|
len(results[results.profit_BTC > 0]),
|
||||||
result.loss.sum()
|
len(results[results.profit_BTC < 0])
|
||||||
])
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
||||||
|
|
||||||
# Append Total
|
def _get_sell_trade_entry(
|
||||||
tabular_data.append([
|
self, pair: str, buy_row: DataFrame,
|
||||||
'TOTAL',
|
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
|
||||||
len(results.index),
|
|
||||||
results.profit_percent.mean() * 100.0,
|
|
||||||
results.profit_BTC.sum(),
|
|
||||||
results.duration.mean() * ticker_interval,
|
|
||||||
results.profit.sum(),
|
|
||||||
results.loss.sum()
|
|
||||||
])
|
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
|
||||||
|
|
||||||
|
stake_amount = args['stake_amount']
|
||||||
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
|
trade = Trade(
|
||||||
|
open_rate=buy_row.close,
|
||||||
|
open_date=buy_row.date,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
amount=stake_amount / buy_row.open,
|
||||||
|
fee=exchange.get_fee()
|
||||||
|
)
|
||||||
|
|
||||||
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
# calculate win/lose forwards from buy point
|
||||||
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False,
|
for sell_row in partial_ticker:
|
||||||
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Implements backtesting functionality
|
|
||||||
:param stake_amount: btc amount to use for each trade
|
|
||||||
:param processed: a processed dictionary with format {pair, data}
|
|
||||||
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
|
||||||
:param realistic: do we try to simulate realistic trades? (default: True)
|
|
||||||
:return: DataFrame
|
|
||||||
"""
|
|
||||||
trades = []
|
|
||||||
trade_count_lock: dict = {}
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
for pair, pair_data in processed.items():
|
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0
|
|
||||||
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
|
||||||
# for each buy point
|
|
||||||
lock_pair_until = None
|
|
||||||
buy_subset = ticker[ticker.buy == 1][['buy', 'open', 'close', 'date', 'sell']]
|
|
||||||
for row in buy_subset.itertuples(index=True):
|
|
||||||
if realistic:
|
|
||||||
if lock_pair_until is not None and row.Index <= lock_pair_until:
|
|
||||||
continue
|
|
||||||
if max_open_trades > 0:
|
if max_open_trades > 0:
|
||||||
# Check if max_open_trades has already been reached for the given date
|
# Increase trade_count_lock for every iteration
|
||||||
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||||
continue
|
|
||||||
|
|
||||||
if max_open_trades > 0:
|
buy_signal = sell_row.buy
|
||||||
# Increase lock
|
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
||||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
sell_row.sell):
|
||||||
|
return \
|
||||||
|
sell_row, \
|
||||||
|
(
|
||||||
|
pair,
|
||||||
|
trade.calc_profit_percent(rate=sell_row.close),
|
||||||
|
trade.calc_profit(rate=sell_row.close),
|
||||||
|
(sell_row.date - buy_row.date).seconds // 60
|
||||||
|
), \
|
||||||
|
sell_row.date
|
||||||
|
return None
|
||||||
|
|
||||||
trade = Trade(
|
def backtest(self, args: Dict) -> DataFrame:
|
||||||
open_rate=row.close,
|
"""
|
||||||
open_date=row.date,
|
Implements backtesting functionality
|
||||||
stake_amount=stake_amount,
|
|
||||||
amount=stake_amount / row.open,
|
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
|
||||||
fee=exchange.get_fee()
|
Of course try to not have ugly code. By some accessor are sometime slower than functions.
|
||||||
|
Avoid, logging on this method
|
||||||
|
|
||||||
|
:param args: a dict containing:
|
||||||
|
stake_amount: btc amount to use for each trade
|
||||||
|
processed: a processed dictionary with format {pair, data}
|
||||||
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
|
processed = args['processed']
|
||||||
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
|
realistic = args.get('realistic', False)
|
||||||
|
record = args.get('record', None)
|
||||||
|
records = []
|
||||||
|
trades = []
|
||||||
|
trade_count_lock = {}
|
||||||
|
for pair, pair_data in processed.items():
|
||||||
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
|
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
|
||||||
|
ticker = [x for x in ticker_data.itertuples()]
|
||||||
|
|
||||||
|
lock_pair_until = None
|
||||||
|
for index, row in enumerate(ticker):
|
||||||
|
if row.buy == 0 or row.sell == 1:
|
||||||
|
continue # skip rows where no buy signal or that would immediately sell off
|
||||||
|
|
||||||
|
if realistic:
|
||||||
|
if lock_pair_until is not None and row.date <= lock_pair_until:
|
||||||
|
continue
|
||||||
|
if max_open_trades > 0:
|
||||||
|
# Check if max_open_trades has already been reached for the given date
|
||||||
|
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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_count_lock, args)
|
||||||
|
|
||||||
|
if ret:
|
||||||
|
row2, trade_entry, next_date = ret
|
||||||
|
lock_pair_until = next_date
|
||||||
|
trades.append(trade_entry)
|
||||||
|
if record:
|
||||||
|
# Note, need to be json.dump friendly
|
||||||
|
# record a tuple of pair, current_profit_percent,
|
||||||
|
# entry-date, duration
|
||||||
|
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')
|
||||||
|
file_dump_json('backtest-result.json', records)
|
||||||
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Run a backtesting end-to-end
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
pairs = self.config['exchange']['pair_whitelist']
|
||||||
|
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||||
|
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||||
|
|
||||||
|
if self.config.get('live'):
|
||||||
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
|
for pair in pairs:
|
||||||
|
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
|
||||||
|
else:
|
||||||
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
|
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
||||||
|
data = optimize.load_data(
|
||||||
|
self.config['datadir'],
|
||||||
|
pairs=pairs,
|
||||||
|
ticker_interval=self.ticker_interval,
|
||||||
|
refresh_pairs=self.config.get('refresh_pairs', False),
|
||||||
|
timerange=timerange
|
||||||
)
|
)
|
||||||
|
|
||||||
# calculate win/lose forwards from buy point
|
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
||||||
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
|
if self.config.get('realistic_simulation', False):
|
||||||
for row2 in sell_subset.itertuples(index=True):
|
max_open_trades = self.config['max_open_trades']
|
||||||
if max_open_trades > 0:
|
else:
|
||||||
# Increase trade_count_lock for every iteration
|
logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
|
||||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
max_open_trades = 0
|
||||||
|
|
||||||
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
preprocessed = self.tickerdata_to_dataframe(data)
|
||||||
if (sell_profit_only and current_profit_percent < 0):
|
|
||||||
continue
|
|
||||||
if min_roi_reached(trade, row2.close, row2.date) or \
|
|
||||||
(row2.sell == 1 and use_sell_signal) or \
|
|
||||||
current_profit_percent <= stoploss:
|
|
||||||
current_profit_btc = trade.calc_profit(rate=row2.close)
|
|
||||||
lock_pair_until = row2.Index
|
|
||||||
|
|
||||||
trades.append(
|
# Print timeframe
|
||||||
(
|
min_date, max_date = self.get_timeframe(preprocessed)
|
||||||
pair,
|
logger.info(
|
||||||
current_profit_percent,
|
'Measuring data from %s up to %s (%s days)..',
|
||||||
current_profit_btc,
|
min_date.isoformat(),
|
||||||
row2.Index - row.Index,
|
max_date.isoformat(),
|
||||||
current_profit_btc > 0,
|
(max_date - min_date).days
|
||||||
current_profit_btc < 0
|
)
|
||||||
)
|
|
||||||
)
|
# Execute backtest and print results
|
||||||
break
|
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
|
||||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
|
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
results = self.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
|
'processed': preprocessed,
|
||||||
|
'max_open_trades': max_open_trades,
|
||||||
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
|
'sell_profit_only': sell_profit_only,
|
||||||
|
'use_sell_signal': use_sell_signal,
|
||||||
|
'record': self.config.get('export')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
'\n==================================== '
|
||||||
|
'BACKTESTING REPORT'
|
||||||
|
' ====================================\n'
|
||||||
|
'%s',
|
||||||
|
self._generate_text_table(
|
||||||
|
data,
|
||||||
|
results
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def start(args):
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
# Initialize logger
|
"""
|
||||||
logging.basicConfig(
|
Prepare the configuration for the backtesting
|
||||||
level=args.loglevel,
|
:param args: Cli args from Arguments()
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
:return: Configuration
|
||||||
)
|
"""
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
# Ensure we do not use Exchange credentials
|
||||||
|
config['exchange']['key'] = ''
|
||||||
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
logger.info('Using config: %s ...', args.config)
|
return config
|
||||||
config = misc.load_config(args.config)
|
|
||||||
|
|
||||||
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
|
|
||||||
|
|
||||||
data = {}
|
def start(args: Namespace) -> None:
|
||||||
pairs = config['exchange']['pair_whitelist']
|
"""
|
||||||
if args.live:
|
Start Backtesting script
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
:param args: Cli args from Arguments()
|
||||||
for pair in pairs:
|
:return: None
|
||||||
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
|
"""
|
||||||
else:
|
# Initialize configuration
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
config = setup_configuration(args)
|
||||||
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval,
|
logger.info('Starting freqtrade in Backtesting mode')
|
||||||
refresh_pairs=args.refresh_pairs)
|
|
||||||
|
|
||||||
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
# Initialize backtesting object
|
||||||
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
backtesting = Backtesting(config)
|
||||||
|
backtesting.start()
|
||||||
max_open_trades = 0
|
|
||||||
if args.realistic_simulation:
|
|
||||||
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
|
|
||||||
max_open_trades = config['max_open_trades']
|
|
||||||
|
|
||||||
# Monkey patch config
|
|
||||||
from freqtrade import main
|
|
||||||
main._CONF = config
|
|
||||||
|
|
||||||
preprocessed = preprocess(data)
|
|
||||||
# Print timeframe
|
|
||||||
min_date, max_date = get_timeframe(preprocessed)
|
|
||||||
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
|
|
||||||
|
|
||||||
# Execute backtest and print results
|
|
||||||
results = backtest(
|
|
||||||
stake_amount=config['stake_amount'],
|
|
||||||
processed=preprocessed,
|
|
||||||
max_open_trades=max_open_trades,
|
|
||||||
realistic=args.realistic_simulation,
|
|
||||||
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False),
|
|
||||||
stoploss=config.get('stoploss'),
|
|
||||||
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False)
|
|
||||||
)
|
|
||||||
logger.info(
|
|
||||||
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
|
|
||||||
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
|
|
||||||
)
|
|
||||||
|
@ -1,318 +1,608 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,W0603
|
# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module contains the hyperopt logic
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import signal
|
import signal
|
||||||
import os
|
import sys
|
||||||
|
from argparse import Namespace
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import exp
|
from math import exp
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from typing import Dict, Any, Callable
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
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 hyperopt.mongoexp import MongoTrials
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import main # noqa
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade import exchange, optimize
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.misc import load_config
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.optimize.backtesting import backtest
|
from freqtrade.optimize.backtesting import Backtesting
|
||||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
|
||||||
|
|
||||||
# Remove noisy log messages
|
|
||||||
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
|
|
||||||
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
|
||||||
TARGET_TRADES = 1100
|
|
||||||
TOTAL_TRIES = 0
|
|
||||||
_CURRENT_TRIES = 0
|
|
||||||
CURRENT_BEST_LOSS = 100
|
|
||||||
|
|
||||||
# max average trade duration in minutes
|
class Hyperopt(Backtesting):
|
||||||
# if eval ends with higher value, we consider it a failed eval
|
"""
|
||||||
MAX_ACCEPTED_TRADE_DURATION = 240
|
Hyperopt class, this class contains all the logic to run a hyperopt simulation
|
||||||
|
|
||||||
# this is expexted avg profit * expected trade count
|
To run a backtest:
|
||||||
# for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85
|
hyperopt = Hyperopt(config)
|
||||||
EXPECTED_MAX_PROFIT = 3.85
|
hyperopt.start()
|
||||||
|
"""
|
||||||
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
# Configuration and data used by hyperopt
|
super().__init__(config)
|
||||||
PROCESSED = None # optimize.preprocess(optimize.load_data())
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic
|
||||||
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
|
# to the number of days
|
||||||
|
self.target_trades = 600
|
||||||
|
self.total_tries = config.get('epochs', 0)
|
||||||
|
self.current_tries = 0
|
||||||
|
self.current_best_loss = 100
|
||||||
|
|
||||||
# Hyperopt Trials
|
# max average trade duration in minutes
|
||||||
TRIALS_FILE = os.path.join('freqtrade', 'optimize', 'hyperopt_trials.pickle')
|
# if eval ends with higher value, we consider it a failed eval
|
||||||
TRIALS = Trials()
|
self.max_accepted_trade_duration = 300
|
||||||
|
|
||||||
# Monkey patch config
|
# this is expexted avg profit * expected trade count
|
||||||
from freqtrade import main # noqa
|
# for example 3.5%, 1100 trades, self.expected_max_profit = 3.85
|
||||||
main._CONF = OPTIMIZE_CONFIG
|
# check that the reported Σ% values do not exceed this!
|
||||||
|
self.expected_max_profit = 3.0
|
||||||
|
|
||||||
|
# Configuration and data used by hyperopt
|
||||||
|
self.processed = None
|
||||||
|
|
||||||
SPACE = {
|
# Hyperopt Trials
|
||||||
'mfi': hp.choice('mfi', [
|
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
||||||
{'enabled': False},
|
self.trials = Trials()
|
||||||
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
|
|
||||||
]),
|
|
||||||
'fastd': hp.choice('fastd', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
|
|
||||||
]),
|
|
||||||
'adx': hp.choice('adx', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
|
||||||
]),
|
|
||||||
'rsi': hp.choice('rsi', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
|
||||||
]),
|
|
||||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True}
|
|
||||||
]),
|
|
||||||
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True}
|
|
||||||
]),
|
|
||||||
'over_sar': hp.choice('over_sar', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True}
|
|
||||||
]),
|
|
||||||
'green_candle': hp.choice('green_candle', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True}
|
|
||||||
]),
|
|
||||||
'uptrend_sma': hp.choice('uptrend_sma', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True}
|
|
||||||
]),
|
|
||||||
'trigger': hp.choice('trigger', [
|
|
||||||
{'type': 'lower_bb'},
|
|
||||||
{'type': 'faststoch10'},
|
|
||||||
{'type': 'ao_cross_zero'},
|
|
||||||
{'type': 'ema5_cross_ema10'},
|
|
||||||
{'type': 'macd_cross_signal'},
|
|
||||||
{'type': 'sar_reversal'},
|
|
||||||
{'type': 'stochf_cross'},
|
|
||||||
{'type': 'ht_sine'},
|
|
||||||
]),
|
|
||||||
'stoploss': hp.uniform('stoploss', -0.5, -0.02),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
"""
|
||||||
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||||
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
|
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||||
|
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||||
|
dataframe['roc'] = ta.ROC(dataframe)
|
||||||
|
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)
|
||||||
|
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||||
|
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||||
|
# Stoch
|
||||||
|
stoch = ta.STOCH(dataframe)
|
||||||
|
dataframe['slowd'] = stoch['slowd']
|
||||||
|
dataframe['slowk'] = stoch['slowk']
|
||||||
|
# Stoch fast
|
||||||
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
dataframe['fastk'] = stoch_fast['fastk']
|
||||||
|
# Stoch RSI
|
||||||
|
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||||
|
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||||
|
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||||
|
# Bollinger bands
|
||||||
|
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']
|
||||||
|
# EMA - Exponential Moving Average
|
||||||
|
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||||
|
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||||
|
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||||
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
# SAR Parabolic
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
# SMA - Simple Moving Average
|
||||||
|
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||||
|
# TEMA - Triple Exponential Moving Average
|
||||||
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
|
# Hilbert Transform Indicator - SineWave
|
||||||
|
hilbert = ta.HT_SINE(dataframe)
|
||||||
|
dataframe['htsine'] = hilbert['sine']
|
||||||
|
dataframe['htleadsine'] = hilbert['leadsine']
|
||||||
|
|
||||||
def save_trials(trials, trials_path=TRIALS_FILE):
|
# Pattern Recognition - Bullish candlestick patterns
|
||||||
"""Save hyperopt trials to file"""
|
# ------------------------------------
|
||||||
logger.info('Saving Trials to \'{}\''.format(trials_path))
|
"""
|
||||||
pickle.dump(trials, open(trials_path, 'wb'))
|
# Hammer: values [0, 100]
|
||||||
|
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||||
|
# Inverted Hammer: values [0, 100]
|
||||||
|
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||||
|
# Dragonfly Doji: values [0, 100]
|
||||||
|
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||||
|
# Piercing Line: values [0, 100]
|
||||||
|
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||||
|
# Morningstar: values [0, 100]
|
||||||
|
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||||
|
# Three White Soldiers: values [0, 100]
|
||||||
|
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pattern Recognition - Bearish candlestick patterns
|
||||||
|
# ------------------------------------
|
||||||
|
"""
|
||||||
|
# Hanging Man: values [0, 100]
|
||||||
|
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||||
|
# Shooting Star: values [0, 100]
|
||||||
|
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||||
|
# Gravestone Doji: values [0, 100]
|
||||||
|
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||||
|
# Dark Cloud Cover: values [0, 100]
|
||||||
|
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||||
|
# Evening Doji Star: values [0, 100]
|
||||||
|
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||||
|
# Evening Star: values [0, 100]
|
||||||
|
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||||
|
"""
|
||||||
|
|
||||||
def read_trials(trials_path=TRIALS_FILE):
|
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||||
"""Read hyperopt trials file"""
|
# ------------------------------------
|
||||||
logger.info('Reading Trials from \'{}\''.format(trials_path))
|
"""
|
||||||
trials = pickle.load(open(trials_path, 'rb'))
|
# Three Line Strike: values [0, -100, 100]
|
||||||
os.remove(trials_path)
|
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||||
return trials
|
# Spinning Top: values [0, -100, 100]
|
||||||
|
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||||
|
# Engulfing: values [0, -100, 100]
|
||||||
|
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||||
|
# Harami: values [0, -100, 100]
|
||||||
|
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||||
|
# Three Outside Up/Down: values [0, -100, 100]
|
||||||
|
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||||
|
# Three Inside Up/Down: values [0, -100, 100]
|
||||||
|
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Chart type
|
||||||
def log_trials_result(trials):
|
# ------------------------------------
|
||||||
vals = json.dumps(trials.best_trial['misc']['vals'], indent=4)
|
# Heikinashi stategy
|
||||||
results = trials.best_trial['result']['result']
|
heikinashi = qtpylib.heikinashi(dataframe)
|
||||||
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
|
dataframe['ha_open'] = heikinashi['open']
|
||||||
|
dataframe['ha_close'] = heikinashi['close']
|
||||||
|
dataframe['ha_high'] = heikinashi['high']
|
||||||
def log_results(results):
|
dataframe['ha_low'] = heikinashi['low']
|
||||||
""" log results if it is better than any previous evaluation """
|
|
||||||
global CURRENT_BEST_LOSS
|
|
||||||
|
|
||||||
if results['loss'] < CURRENT_BEST_LOSS:
|
|
||||||
CURRENT_BEST_LOSS = results['loss']
|
|
||||||
logger.info('{:5d}/{}: {}'.format(
|
|
||||||
results['current_tries'],
|
|
||||||
results['total_tries'],
|
|
||||||
results['result']))
|
|
||||||
else:
|
|
||||||
print('.', end='')
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_loss(total_profit: float, trade_count: int, trade_duration: float):
|
|
||||||
""" objective function, returns smaller number for more optimal results """
|
|
||||||
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
|
||||||
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
|
||||||
duration_loss = min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
|
|
||||||
return trade_loss + profit_loss + duration_loss
|
|
||||||
|
|
||||||
|
|
||||||
def optimizer(params):
|
|
||||||
global _CURRENT_TRIES
|
|
||||||
|
|
||||||
from freqtrade.optimize import backtesting
|
|
||||||
backtesting.populate_buy_trend = buy_strategy_generator(params)
|
|
||||||
|
|
||||||
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss'])
|
|
||||||
result_explanation = format_results(results)
|
|
||||||
|
|
||||||
total_profit = results.profit_percent.sum()
|
|
||||||
trade_count = len(results.index)
|
|
||||||
trade_duration = results.duration.mean() * 5
|
|
||||||
|
|
||||||
if trade_count == 0 or trade_duration > MAX_ACCEPTED_TRADE_DURATION:
|
|
||||||
print('.', end='')
|
|
||||||
return {
|
|
||||||
'status': STATUS_FAIL,
|
|
||||||
'loss': float('inf')
|
|
||||||
}
|
|
||||||
|
|
||||||
loss = calculate_loss(total_profit, trade_count, trade_duration)
|
|
||||||
|
|
||||||
_CURRENT_TRIES += 1
|
|
||||||
|
|
||||||
log_results({
|
|
||||||
'loss': loss,
|
|
||||||
'current_tries': _CURRENT_TRIES,
|
|
||||||
'total_tries': TOTAL_TRIES,
|
|
||||||
'result': result_explanation,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
'loss': loss,
|
|
||||||
'status': STATUS_OK,
|
|
||||||
'result': result_explanation,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def format_results(results: DataFrame):
|
|
||||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
|
||||||
'Total profit {: 11.8f} BTC. Avg duration {:5.1f} mins.').format(
|
|
||||||
len(results.index),
|
|
||||||
results.profit_percent.mean() * 100.0,
|
|
||||||
results.profit_BTC.sum(),
|
|
||||||
results.duration.mean() * 5,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def buy_strategy_generator(params):
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|
||||||
conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if params['uptrend_long_ema']['enabled']:
|
|
||||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
|
||||||
if params['uptrend_short_ema']['enabled']:
|
|
||||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
|
||||||
if params['mfi']['enabled']:
|
|
||||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
|
||||||
if params['fastd']['enabled']:
|
|
||||||
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
|
||||||
if params['adx']['enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
|
||||||
if params['rsi']['enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
|
||||||
if params['over_sar']['enabled']:
|
|
||||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
|
||||||
if params['green_candle']['enabled']:
|
|
||||||
conditions.append(dataframe['close'] > dataframe['open'])
|
|
||||||
if params['uptrend_sma']['enabled']:
|
|
||||||
prevsma = dataframe['sma'].shift(1)
|
|
||||||
conditions.append(dataframe['sma'] > prevsma)
|
|
||||||
|
|
||||||
# TRIGGERS
|
|
||||||
triggers = {
|
|
||||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
|
||||||
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
|
|
||||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
|
||||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
|
||||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
|
||||||
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
|
|
||||||
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
|
|
||||||
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
|
|
||||||
}
|
|
||||||
conditions.append(triggers.get(params['trigger']['type']))
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
reduce(lambda x, y: x & y, conditions),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
|
||||||
|
|
||||||
|
def save_trials(self) -> None:
|
||||||
|
"""
|
||||||
|
Save hyperopt trials to file
|
||||||
|
"""
|
||||||
|
logger.info('Saving Trials to \'%s\'', self.trials_file)
|
||||||
|
pickle.dump(self.trials, open(self.trials_file, 'wb'))
|
||||||
|
|
||||||
def start(args):
|
def read_trials(self) -> Trials:
|
||||||
global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES
|
"""
|
||||||
|
Read hyperopt trials file
|
||||||
|
"""
|
||||||
|
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||||
|
trials = pickle.load(open(self.trials_file, 'rb'))
|
||||||
|
os.remove(self.trials_file)
|
||||||
|
return trials
|
||||||
|
|
||||||
TOTAL_TRIES = args.epochs
|
def log_trials_result(self) -> None:
|
||||||
|
"""
|
||||||
|
Display Best hyperopt result
|
||||||
|
"""
|
||||||
|
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4)
|
||||||
|
results = self.trials.best_trial['result']['result']
|
||||||
|
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
def log_results(self, results) -> None:
|
||||||
|
"""
|
||||||
|
Log results if it is better than any previous evaluation
|
||||||
|
"""
|
||||||
|
if results['loss'] < self.current_best_loss:
|
||||||
|
self.current_best_loss = results['loss']
|
||||||
|
log_msg = '\n{:5d}/{}: {}. Loss {:.5f}'.format(
|
||||||
|
results['current_tries'],
|
||||||
|
results['total_tries'],
|
||||||
|
results['result'],
|
||||||
|
results['loss']
|
||||||
|
)
|
||||||
|
print(log_msg)
|
||||||
|
else:
|
||||||
|
print('.', end='')
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Initialize logger
|
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
|
||||||
logging.basicConfig(
|
"""
|
||||||
level=args.loglevel,
|
Objective function, returns smaller number for more optimal results
|
||||||
format='\n%(message)s',
|
"""
|
||||||
)
|
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
|
||||||
|
profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
|
||||||
|
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
|
||||||
|
return trade_loss + profit_loss + duration_loss
|
||||||
|
|
||||||
logger.info('Using config: %s ...', args.config)
|
@staticmethod
|
||||||
config = load_config(args.config)
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
pairs = config['exchange']['pair_whitelist']
|
"""
|
||||||
PROCESSED = optimize.preprocess(optimize.load_data(
|
Generate the ROI table thqt will be used by Hyperopt
|
||||||
args.datadir, pairs=pairs, ticker_interval=args.ticker_interval))
|
"""
|
||||||
|
roi_table = {}
|
||||||
|
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||||
|
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
|
||||||
|
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
|
||||||
|
|
||||||
if args.mongodb:
|
return roi_table
|
||||||
logger.info('Using mongodb ...')
|
|
||||||
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
|
|
||||||
|
|
||||||
db_name = 'freqtrade_hyperopt'
|
@staticmethod
|
||||||
TRIALS = MongoTrials('mongo://127.0.0.1:1234/{}/jobs'.format(db_name), exp_key='exp1')
|
def roi_space() -> Dict[str, Any]:
|
||||||
else:
|
"""
|
||||||
logger.info('Preparing Trials..')
|
Values to search for each ROI steps
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
"""
|
||||||
# read trials file if we have one
|
return {
|
||||||
if os.path.exists(TRIALS_FILE):
|
'roi_t1': hp.quniform('roi_t1', 10, 120, 20),
|
||||||
TRIALS = read_trials()
|
'roi_t2': hp.quniform('roi_t2', 10, 60, 15),
|
||||||
|
'roi_t3': hp.quniform('roi_t3', 10, 40, 10),
|
||||||
|
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01),
|
||||||
|
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01),
|
||||||
|
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01),
|
||||||
|
}
|
||||||
|
|
||||||
_CURRENT_TRIES = len(TRIALS.results)
|
@staticmethod
|
||||||
TOTAL_TRIES = TOTAL_TRIES + _CURRENT_TRIES
|
def stoploss_space() -> Dict[str, Any]:
|
||||||
logger.info(
|
"""
|
||||||
'Continuing with trials. Current: {}, Total: {}'
|
Stoploss Value to search
|
||||||
.format(_CURRENT_TRIES, TOTAL_TRIES))
|
"""
|
||||||
|
return {
|
||||||
|
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02),
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
@staticmethod
|
||||||
best_parameters = fmin(
|
def indicator_space() -> Dict[str, Any]:
|
||||||
fn=optimizer,
|
"""
|
||||||
space=SPACE,
|
Define your Hyperopt space for searching strategy parameters
|
||||||
algo=tpe.suggest,
|
"""
|
||||||
max_evals=TOTAL_TRIES,
|
return {
|
||||||
trials=TRIALS
|
'macd_below_zero': hp.choice('macd_below_zero', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'mfi': hp.choice('mfi', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)}
|
||||||
|
]),
|
||||||
|
'fastd': hp.choice('fastd', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.quniform('fastd-value', 15, 45, 5)}
|
||||||
|
]),
|
||||||
|
'adx': hp.choice('adx', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.quniform('adx-value', 20, 50, 5)}
|
||||||
|
]),
|
||||||
|
'rsi': hp.choice('rsi', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 5)}
|
||||||
|
]),
|
||||||
|
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'over_sar': hp.choice('over_sar', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'green_candle': hp.choice('green_candle', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'trigger': hp.choice('trigger', [
|
||||||
|
{'type': 'lower_bb'},
|
||||||
|
{'type': 'lower_bb_tema'},
|
||||||
|
{'type': 'faststoch10'},
|
||||||
|
{'type': 'ao_cross_zero'},
|
||||||
|
{'type': 'ema3_cross_ema10'},
|
||||||
|
{'type': 'macd_cross_signal'},
|
||||||
|
{'type': 'sar_reversal'},
|
||||||
|
{'type': 'ht_sine'},
|
||||||
|
{'type': 'heiken_reversal_bull'},
|
||||||
|
{'type': 'di_cross'},
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_space(self, space: str) -> bool:
|
||||||
|
"""
|
||||||
|
Tell if a space value is contained in the configuration
|
||||||
|
"""
|
||||||
|
if space in self.config['spaces'] or 'all' in self.config['spaces']:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def hyperopt_space(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return the space to use during Hyperopt
|
||||||
|
"""
|
||||||
|
spaces = {}
|
||||||
|
if self.has_space('buy'):
|
||||||
|
spaces = {**spaces, **Hyperopt.indicator_space()}
|
||||||
|
if self.has_space('roi'):
|
||||||
|
spaces = {**spaces, **Hyperopt.roi_space()}
|
||||||
|
if self.has_space('stoploss'):
|
||||||
|
spaces = {**spaces, **Hyperopt.stoploss_space()}
|
||||||
|
return spaces
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
"""
|
||||||
|
Define the buy strategy parameters to be used by hyperopt
|
||||||
|
"""
|
||||||
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Buy strategy Hyperopt will build and use
|
||||||
|
"""
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']:
|
||||||
|
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||||
|
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']:
|
||||||
|
conditions.append(dataframe['macd'] < 0)
|
||||||
|
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']:
|
||||||
|
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
||||||
|
if 'mfi' in params and params['mfi']['enabled']:
|
||||||
|
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||||
|
if 'fastd' in params and params['fastd']['enabled']:
|
||||||
|
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||||
|
if 'adx' in params and params['adx']['enabled']:
|
||||||
|
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||||
|
if 'rsi' in params and params['rsi']['enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||||
|
if 'over_sar' in params and params['over_sar']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||||
|
if 'green_candle' in params and params['green_candle']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] > dataframe['open'])
|
||||||
|
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
|
||||||
|
prevsma = dataframe['sma'].shift(1)
|
||||||
|
conditions.append(dataframe['sma'] > prevsma)
|
||||||
|
|
||||||
|
# TRIGGERS
|
||||||
|
triggers = {
|
||||||
|
'lower_bb': (
|
||||||
|
dataframe['close'] < dataframe['bb_lowerband']
|
||||||
|
),
|
||||||
|
'lower_bb_tema': (
|
||||||
|
dataframe['tema'] < dataframe['bb_lowerband']
|
||||||
|
),
|
||||||
|
'faststoch10': (qtpylib.crossed_above(
|
||||||
|
dataframe['fastd'], 10.0
|
||||||
|
)),
|
||||||
|
'ao_cross_zero': (qtpylib.crossed_above(
|
||||||
|
dataframe['ao'], 0.0
|
||||||
|
)),
|
||||||
|
'ema3_cross_ema10': (qtpylib.crossed_above(
|
||||||
|
dataframe['ema3'], dataframe['ema10']
|
||||||
|
)),
|
||||||
|
'macd_cross_signal': (qtpylib.crossed_above(
|
||||||
|
dataframe['macd'], dataframe['macdsignal']
|
||||||
|
)),
|
||||||
|
'sar_reversal': (qtpylib.crossed_above(
|
||||||
|
dataframe['close'], dataframe['sar']
|
||||||
|
)),
|
||||||
|
'ht_sine': (qtpylib.crossed_above(
|
||||||
|
dataframe['htleadsine'], dataframe['htsine']
|
||||||
|
)),
|
||||||
|
'heiken_reversal_bull': (
|
||||||
|
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
|
||||||
|
(dataframe['ha_low'] == dataframe['ha_open'])
|
||||||
|
),
|
||||||
|
'di_cross': (qtpylib.crossed_above(
|
||||||
|
dataframe['plus_di'], dataframe['minus_di']
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
conditions.append(triggers.get(params['trigger']['type']))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
return populate_buy_trend
|
||||||
|
|
||||||
|
def generate_optimizer(self, params: Dict) -> Dict:
|
||||||
|
if self.has_space('roi'):
|
||||||
|
self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
|
||||||
|
|
||||||
|
if self.has_space('buy'):
|
||||||
|
self.populate_buy_trend = self.buy_strategy_generator(params)
|
||||||
|
|
||||||
|
if self.has_space('stoploss'):
|
||||||
|
self.analyze.strategy.stoploss = params['stoploss']
|
||||||
|
|
||||||
|
results = self.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': self.config['stake_amount'],
|
||||||
|
'processed': self.processed,
|
||||||
|
'realistic': self.config.get('realistic_simulation', False),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result_explanation = self.format_results(results)
|
||||||
|
|
||||||
|
total_profit = results.profit_percent.sum()
|
||||||
|
trade_count = len(results.index)
|
||||||
|
trade_duration = results.duration.mean()
|
||||||
|
|
||||||
|
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
||||||
|
print('.', end='')
|
||||||
|
return {
|
||||||
|
'status': STATUS_FAIL,
|
||||||
|
'loss': float('inf')
|
||||||
|
}
|
||||||
|
|
||||||
|
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
|
||||||
|
|
||||||
|
self.current_tries += 1
|
||||||
|
|
||||||
|
self.log_results(
|
||||||
|
{
|
||||||
|
'loss': loss,
|
||||||
|
'current_tries': self.current_tries,
|
||||||
|
'total_tries': self.total_tries,
|
||||||
|
'result': result_explanation,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
results = sorted(TRIALS.results, key=itemgetter('loss'))
|
return {
|
||||||
best_result = results[0]['result']
|
'loss': loss,
|
||||||
|
'status': STATUS_OK,
|
||||||
|
'result': result_explanation,
|
||||||
|
}
|
||||||
|
|
||||||
except ValueError:
|
@staticmethod
|
||||||
best_parameters = {}
|
def format_results(results: DataFrame) -> str:
|
||||||
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
|
"""
|
||||||
'try with more epochs (param: -e).'
|
Return the format result in a string
|
||||||
|
"""
|
||||||
|
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||||
|
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit_percent.mean() * 100.0,
|
||||||
|
results.profit_BTC.sum(),
|
||||||
|
results.profit_percent.sum(),
|
||||||
|
results.duration.mean(),
|
||||||
|
)
|
||||||
|
|
||||||
# Improve best parameter logging display
|
def start(self) -> None:
|
||||||
if best_parameters:
|
timerange = Arguments.parse_timerange(self.config.get('timerange'))
|
||||||
best_parameters = space_eval(SPACE, best_parameters)
|
data = load_data(
|
||||||
|
datadir=self.config.get('datadir'),
|
||||||
|
pairs=self.config['exchange']['pair_whitelist'],
|
||||||
|
ticker_interval=self.ticker_interval,
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
|
||||||
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
|
if self.has_space('buy'):
|
||||||
logger.info('Best Result:\n%s', best_result)
|
self.analyze.populate_indicators = Hyperopt.populate_indicators
|
||||||
|
self.processed = self.tickerdata_to_dataframe(data)
|
||||||
|
|
||||||
# Store trials result to file to resume next time
|
if self.config.get('mongodb'):
|
||||||
save_trials(TRIALS)
|
logger.info('Using mongodb ...')
|
||||||
|
logger.info(
|
||||||
|
'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!'
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
best_parameters = fmin(
|
||||||
|
fn=self.generate_optimizer,
|
||||||
|
space=self.hyperopt_space(),
|
||||||
|
algo=tpe.suggest,
|
||||||
|
max_evals=self.total_tries,
|
||||||
|
trials=self.trials
|
||||||
|
)
|
||||||
|
|
||||||
|
results = sorted(self.trials.results, key=itemgetter('loss'))
|
||||||
|
best_result = results[0]['result']
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
best_parameters = {}
|
||||||
|
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
|
||||||
|
'try with more epochs (param: -e).'
|
||||||
|
|
||||||
|
# Improve best parameter logging display
|
||||||
|
if best_parameters:
|
||||||
|
best_parameters = space_eval(
|
||||||
|
self.hyperopt_space(),
|
||||||
|
best_parameters
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
|
||||||
|
if 'roi_t1' in best_parameters:
|
||||||
|
logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
|
||||||
|
|
||||||
|
logger.info('Best Result:\n%s', best_result)
|
||||||
|
|
||||||
|
# Store trials result to file to resume next time
|
||||||
|
self.save_trials()
|
||||||
|
|
||||||
|
def signal_handler(self, sig, frame) -> None:
|
||||||
|
"""
|
||||||
|
Hyperopt SIGINT handler
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
'Hyperopt received %s',
|
||||||
|
signal.Signals(sig).name
|
||||||
|
)
|
||||||
|
|
||||||
|
self.save_trials()
|
||||||
|
self.log_trials_result()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
def start(args: Namespace) -> None:
|
||||||
"""Hyperopt SIGINT handler"""
|
"""
|
||||||
logger.info('Hyperopt received {}'.format(signal.Signals(sig).name))
|
Start Backtesting script
|
||||||
|
:param args: Cli args from Arguments()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
save_trials(TRIALS)
|
# Remove noisy log messages
|
||||||
log_trials_result(TRIALS)
|
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
|
||||||
sys.exit(0)
|
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Initialize configuration
|
||||||
|
# Monkey patch the configuration with hyperopt_conf.py
|
||||||
|
configuration = Configuration(args)
|
||||||
|
logger.info('Starting freqtrade in Hyperopt mode')
|
||||||
|
|
||||||
|
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']['secret'] = ''
|
||||||
|
|
||||||
|
# Initialize backtesting object
|
||||||
|
hyperopt = Hyperopt(config)
|
||||||
|
hyperopt.start()
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
This module contains the class to persist trades into SQLite
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
@ -47,6 +51,10 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
|
|||||||
Trade.query = session.query_property()
|
Trade.query = session.query_property()
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
|
||||||
|
# Clean dry_run DB
|
||||||
|
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
|
||||||
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
@ -56,7 +64,21 @@ def cleanup() -> None:
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def clean_dry_run_db() -> None:
|
||||||
|
"""
|
||||||
|
Remove open_order_id from a Dry_run DB
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
|
# Check we are updating only a dry_run order not a prod one
|
||||||
|
if 'dry_run' in trade.open_order_id:
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
|
|
||||||
class Trade(_DECL_BASE):
|
class Trade(_DECL_BASE):
|
||||||
|
"""
|
||||||
|
Class used to define a trade structure
|
||||||
|
"""
|
||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
@ -172,8 +194,8 @@ class Trade(_DECL_BASE):
|
|||||||
"""
|
"""
|
||||||
open_trade_price = self.calc_open_trade_price()
|
open_trade_price = self.calc_open_trade_price()
|
||||||
close_trade_price = self.calc_close_trade_price(
|
close_trade_price = self.calc_close_trade_price(
|
||||||
rate=Decimal(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=Decimal(fee or self.fee)
|
fee=(fee or self.fee)
|
||||||
)
|
)
|
||||||
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
||||||
|
|
||||||
@ -185,14 +207,15 @@ class Trade(_DECL_BASE):
|
|||||||
Calculates the profit in percentage (including fee).
|
Calculates the profit in percentage (including fee).
|
||||||
:param rate: rate to compare with (optional).
|
:param rate: rate to compare with (optional).
|
||||||
If rate is not set self.close_rate will be used
|
If rate is not set self.close_rate will be used
|
||||||
|
:param fee: fee to use on the close rate (optional).
|
||||||
:return: profit in percentage as float
|
:return: profit in percentage as float
|
||||||
"""
|
"""
|
||||||
getcontext().prec = 8
|
getcontext().prec = 8
|
||||||
|
|
||||||
open_trade_price = self.calc_open_trade_price()
|
open_trade_price = self.calc_open_trade_price()
|
||||||
close_trade_price = self.calc_close_trade_price(
|
close_trade_price = self.calc_close_trade_price(
|
||||||
rate=Decimal(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=Decimal(fee or self.fee)
|
fee=(fee or self.fee)
|
||||||
)
|
)
|
||||||
|
|
||||||
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from . import telegram
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
REGISTERED_MODULES = []
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
|
||||||
"""
|
|
||||||
Initializes all enabled rpc modules
|
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
if config['telegram'].get('enabled', False):
|
|
||||||
logger.info('Enabling rpc.telegram ...')
|
|
||||||
REGISTERED_MODULES.append('telegram')
|
|
||||||
telegram.init(config)
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
|
||||||
"""
|
|
||||||
Stops all enabled rpc modules
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if 'telegram' in REGISTERED_MODULES:
|
|
||||||
logger.debug('Cleaning up rpc.telegram ...')
|
|
||||||
telegram.cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
def send_msg(msg: str) -> None:
|
|
||||||
"""
|
|
||||||
Send given markdown message to all registered rpc modules
|
|
||||||
:param msg: message
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
logger.info(msg)
|
|
||||||
if 'telegram' in REGISTERED_MODULES:
|
|
||||||
telegram.send_msg(msg)
|
|
383
freqtrade/rpc/rpc.py
Normal file
383
freqtrade/rpc/rpc.py
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
"""
|
||||||
|
This module contains class to define a RPC communications
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Tuple, Any
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import sqlalchemy as sql
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.misc import shorten_date
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RPC(object):
|
||||||
|
"""
|
||||||
|
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||||
|
"""
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.freqtrade = freqtrade
|
||||||
|
|
||||||
|
def rpc_trade_status(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||||
|
a remotely exposed function
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# Fetch open trade
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
if self.freqtrade.state != State.RUNNING:
|
||||||
|
return True, '*Status:* `trader is not running`'
|
||||||
|
elif not trades:
|
||||||
|
return True, '*Status:* `no active trade`'
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
for trade in trades:
|
||||||
|
order = None
|
||||||
|
if trade.open_order_id:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
# calculate profit and send message to user
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
|
round(trade.close_profit * 100, 2)
|
||||||
|
) if trade.close_profit else None
|
||||||
|
message = "*Trade ID:* `{trade_id}`\n" \
|
||||||
|
"*Current Pair:* [{pair}]({market_url})\n" \
|
||||||
|
"*Open Since:* `{date}`\n" \
|
||||||
|
"*Amount:* `{amount}`\n" \
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||||
|
"*Close Rate:* `{close_rate}`\n" \
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
|
"*Close Profit:* `{close_profit}`\n" \
|
||||||
|
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
||||||
|
"*Open Order:* `{open_order}`"\
|
||||||
|
.format(
|
||||||
|
trade_id=trade.id,
|
||||||
|
pair=trade.pair,
|
||||||
|
market_url=exchange.get_pair_detail_url(trade.pair),
|
||||||
|
date=arrow.get(trade.open_date).humanize(),
|
||||||
|
open_rate=trade.open_rate,
|
||||||
|
close_rate=trade.close_rate,
|
||||||
|
current_rate=current_rate,
|
||||||
|
amount=round(trade.amount, 8),
|
||||||
|
close_profit=fmt_close_profit,
|
||||||
|
current_profit=round(current_profit * 100, 2),
|
||||||
|
open_order='({} rem={:.8f})'.format(
|
||||||
|
order['type'], order['remaining']
|
||||||
|
) if order else None,
|
||||||
|
)
|
||||||
|
result.append(message)
|
||||||
|
return False, result
|
||||||
|
|
||||||
|
def rpc_status_table(self) -> Tuple[bool, Any]:
|
||||||
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
if self.freqtrade.state != State.RUNNING:
|
||||||
|
return True, '*Status:* `trader is not running`'
|
||||||
|
elif not trades:
|
||||||
|
return True, '*Status:* `no active order`'
|
||||||
|
else:
|
||||||
|
trades_list = []
|
||||||
|
for trade in trades:
|
||||||
|
# calculate profit and send message to user
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
trades_list.append([
|
||||||
|
trade.id,
|
||||||
|
trade.pair,
|
||||||
|
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||||
|
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
||||||
|
])
|
||||||
|
|
||||||
|
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||||
|
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||||
|
df_statuses = df_statuses.set_index(columns[0])
|
||||||
|
# The style used throughout is to return a tuple
|
||||||
|
# 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(
|
||||||
|
self, timescale: int,
|
||||||
|
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
today = datetime.utcnow().date()
|
||||||
|
profit_days = {}
|
||||||
|
|
||||||
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
|
return True, '*Daily [n]:* `must be an integer greater than 0`'
|
||||||
|
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
for day in range(0, timescale):
|
||||||
|
profitday = today - timedelta(days=day)
|
||||||
|
trades = Trade.query \
|
||||||
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.filter(Trade.close_date >= profitday)\
|
||||||
|
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
|
||||||
|
.order_by(Trade.close_date)\
|
||||||
|
.all()
|
||||||
|
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||||
|
profit_days[profitday] = {
|
||||||
|
'amount': format(curdayprofit, '.8f'),
|
||||||
|
'trades': len(trades)
|
||||||
|
}
|
||||||
|
|
||||||
|
stats = [
|
||||||
|
[
|
||||||
|
key,
|
||||||
|
'{value:.8f} {symbol}'.format(
|
||||||
|
value=float(value['amount']),
|
||||||
|
symbol=stake_currency
|
||||||
|
),
|
||||||
|
'{value:.3f} {symbol}'.format(
|
||||||
|
value=fiat.convert_amount(
|
||||||
|
value['amount'],
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
),
|
||||||
|
symbol=fiat_display_currency
|
||||||
|
),
|
||||||
|
'{value} trade{s}'.format(
|
||||||
|
value=value['trades'],
|
||||||
|
s='' if value['trades'] < 2 else 's'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for key, value in profit_days.items()
|
||||||
|
]
|
||||||
|
return False, stats
|
||||||
|
|
||||||
|
def rpc_trade_statistics(
|
||||||
|
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
:return: cumulative profit statistics.
|
||||||
|
"""
|
||||||
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
|
profit_all_coin = []
|
||||||
|
profit_all_percent = []
|
||||||
|
profit_closed_coin = []
|
||||||
|
profit_closed_percent = []
|
||||||
|
durations = []
|
||||||
|
|
||||||
|
for trade in trades:
|
||||||
|
current_rate = None
|
||||||
|
|
||||||
|
if not trade.open_rate:
|
||||||
|
continue
|
||||||
|
if trade.close_date:
|
||||||
|
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||||
|
|
||||||
|
if not trade.is_open:
|
||||||
|
profit_percent = trade.calc_profit_percent()
|
||||||
|
profit_closed_coin.append(trade.calc_profit())
|
||||||
|
profit_closed_percent.append(profit_percent)
|
||||||
|
else:
|
||||||
|
# Get current rate
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||||
|
|
||||||
|
profit_all_coin.append(
|
||||||
|
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
|
||||||
|
)
|
||||||
|
profit_all_percent.append(profit_percent)
|
||||||
|
|
||||||
|
best_pair = Trade.session.query(
|
||||||
|
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
||||||
|
).filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(sql.text('profit_sum DESC')).first()
|
||||||
|
|
||||||
|
if not best_pair:
|
||||||
|
return True, '*Status:* `no closed trade`'
|
||||||
|
|
||||||
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
|
# FIX: we want to keep fiatconverter in a state/environment,
|
||||||
|
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
# Prepare data to display
|
||||||
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
|
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
||||||
|
profit_closed_fiat = fiat.convert_amount(
|
||||||
|
profit_closed_coin,
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
)
|
||||||
|
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||||
|
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
||||||
|
profit_all_fiat = fiat.convert_amount(
|
||||||
|
profit_all_coin,
|
||||||
|
stake_currency,
|
||||||
|
fiat_display_currency
|
||||||
|
)
|
||||||
|
num = float(len(durations) or 1)
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
'profit_closed_coin': profit_closed_coin,
|
||||||
|
'profit_closed_percent': profit_closed_percent,
|
||||||
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
|
'profit_all_coin': profit_all_coin,
|
||||||
|
'profit_all_percent': profit_all_percent,
|
||||||
|
'profit_all_fiat': profit_all_fiat,
|
||||||
|
'trade_count': len(trades),
|
||||||
|
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
|
||||||
|
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
|
||||||
|
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||||
|
'best_pair': bp_pair,
|
||||||
|
'best_rate': round(bp_rate * 100, 2)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
:return: current account balance per crypto
|
||||||
|
"""
|
||||||
|
balances = [
|
||||||
|
c for c in exchange.get_balances()
|
||||||
|
if c['Balance'] or c['Available'] or c['Pending']
|
||||||
|
]
|
||||||
|
if not balances:
|
||||||
|
return True, '`All balances are zero.`'
|
||||||
|
|
||||||
|
output = []
|
||||||
|
total = 0.0
|
||||||
|
for currency in balances:
|
||||||
|
coin = currency['Currency']
|
||||||
|
if coin == 'BTC':
|
||||||
|
currency["Rate"] = 1.0
|
||||||
|
else:
|
||||||
|
if coin == 'USDT':
|
||||||
|
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
|
||||||
|
else:
|
||||||
|
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
|
||||||
|
currency['BTC'] = currency["Rate"] * currency["Balance"]
|
||||||
|
total = total + currency['BTC']
|
||||||
|
output.append(
|
||||||
|
{
|
||||||
|
'currency': currency['Currency'],
|
||||||
|
'available': currency['Available'],
|
||||||
|
'balance': currency['Balance'],
|
||||||
|
'pending': currency['Pending'],
|
||||||
|
'est_btc': currency['BTC']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fiat = self.freqtrade.fiat_converter
|
||||||
|
symbol = fiat_display_currency
|
||||||
|
value = fiat.convert_amount(total, 'BTC', symbol)
|
||||||
|
return False, (output, total, symbol, value)
|
||||||
|
|
||||||
|
def rpc_start(self) -> (bool, str):
|
||||||
|
"""
|
||||||
|
Handler for start.
|
||||||
|
"""
|
||||||
|
if self.freqtrade.state == State.RUNNING:
|
||||||
|
return True, '*Status:* `already running`'
|
||||||
|
|
||||||
|
self.freqtrade.state = State.RUNNING
|
||||||
|
return False, '`Starting trader ...`'
|
||||||
|
|
||||||
|
def rpc_stop(self) -> (bool, str):
|
||||||
|
"""
|
||||||
|
Handler for stop.
|
||||||
|
"""
|
||||||
|
if self.freqtrade.state == State.RUNNING:
|
||||||
|
self.freqtrade.state = State.STOPPED
|
||||||
|
return False, '`Stopping trader ...`'
|
||||||
|
|
||||||
|
return True, '*Status:* `already stopped`'
|
||||||
|
|
||||||
|
# FIX: no test for this!!!!
|
||||||
|
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Handler for forcesell <id>.
|
||||||
|
Sells the given trade at current price
|
||||||
|
:return: error or None
|
||||||
|
"""
|
||||||
|
def _exec_forcesell(trade: Trade) -> None:
|
||||||
|
# Check if there is there is an open order
|
||||||
|
if trade.open_order_id:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
|
|
||||||
|
# Cancel open LIMIT_BUY orders and close trade
|
||||||
|
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
|
||||||
|
exchange.cancel_order(trade.open_order_id)
|
||||||
|
trade.close(order.get('rate') or trade.open_rate)
|
||||||
|
# TODO: sell amount which has been bought already
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ignore trades with an attached LIMIT_SELL order
|
||||||
|
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current rate and execute sell
|
||||||
|
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
self.freqtrade.execute_sell(trade, current_rate)
|
||||||
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
|
if self.freqtrade.state != State.RUNNING:
|
||||||
|
return True, '`trader is not running`'
|
||||||
|
|
||||||
|
if trade_id == 'all':
|
||||||
|
# Execute sell for all open orders
|
||||||
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
|
_exec_forcesell(trade)
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
# Query for trade
|
||||||
|
trade = Trade.query.filter(
|
||||||
|
sql.and_(
|
||||||
|
Trade.id == trade_id,
|
||||||
|
Trade.is_open.is_(True)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if not trade:
|
||||||
|
logger.warning('forcesell: Invalid argument received')
|
||||||
|
return True, 'Invalid argument.'
|
||||||
|
|
||||||
|
_exec_forcesell(trade)
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
def rpc_performance(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
Handler for performance.
|
||||||
|
Shows a performance statistic from finished trades
|
||||||
|
"""
|
||||||
|
if self.freqtrade.state != State.RUNNING:
|
||||||
|
return True, '`trader is not running`'
|
||||||
|
|
||||||
|
pair_rates = Trade.session.query(Trade.pair,
|
||||||
|
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
sql.func.count(Trade.pair).label('count')) \
|
||||||
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(sql.text('profit_sum DESC')) \
|
||||||
|
.all()
|
||||||
|
trades = []
|
||||||
|
for (pair, rate, count) in pair_rates:
|
||||||
|
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
|
||||||
|
|
||||||
|
return False, trades
|
||||||
|
|
||||||
|
def rpc_count(self) -> Tuple[bool, Any]:
|
||||||
|
"""
|
||||||
|
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
|
56
freqtrade/rpc/rpc_manager.py
Normal file
56
freqtrade/rpc/rpc_manager.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade.rpc.telegram import Telegram
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCManager(object):
|
||||||
|
"""
|
||||||
|
Class to manage RPC objects (Telegram, Slack, ...)
|
||||||
|
"""
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.freqtrade = freqtrade
|
||||||
|
|
||||||
|
self.registered_modules = []
|
||||||
|
self.telegram = None
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
def _init(self) -> None:
|
||||||
|
"""
|
||||||
|
Init RPC modules
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.freqtrade.config['telegram'].get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.telegram ...')
|
||||||
|
self.registered_modules.append('telegram')
|
||||||
|
self.telegram = Telegram(self.freqtrade)
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Stops all enabled rpc modules
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if 'telegram' in self.registered_modules:
|
||||||
|
logger.info('Cleaning up rpc.telegram ...')
|
||||||
|
self.registered_modules.remove('telegram')
|
||||||
|
self.telegram.cleanup()
|
||||||
|
|
||||||
|
def send_msg(self, msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message to all registered rpc modules
|
||||||
|
:param msg: message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info(msg)
|
||||||
|
if 'telegram' in self.registered_modules:
|
||||||
|
self.telegram.send_msg(msg)
|
File diff suppressed because it is too large
Load Diff
14
freqtrade/state.py
Normal file
14
freqtrade/state.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# pragma pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
"""
|
||||||
|
Bot state constant
|
||||||
|
"""
|
||||||
|
import enum
|
||||||
|
|
||||||
|
|
||||||
|
class State(enum.Enum):
|
||||||
|
"""
|
||||||
|
Bot running states
|
||||||
|
"""
|
||||||
|
RUNNING = 0
|
||||||
|
STOPPED = 1
|
0
freqtrade/strategy/__init__.py
Normal file
0
freqtrade/strategy/__init__.py
Normal file
240
freqtrade/strategy/default_strategy.py
Normal file
240
freqtrade/strategy/default_strategy.py
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
from freqtrade.indicator_helpers import fishers_inverse
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultStrategy(IStrategy):
|
||||||
|
"""
|
||||||
|
Default Strategy provided by freqtrade bot.
|
||||||
|
You can override it with your own strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy
|
||||||
|
minimal_roi = {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
stoploss = -0.10
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = 5
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
|
Performance Note: For the best performance be frugal on the number of indicators
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Momentum Indicator
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
# ADX
|
||||||
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
|
||||||
|
# Awesome oscillator
|
||||||
|
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||||
|
"""
|
||||||
|
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||||
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
|
"""
|
||||||
|
# MACD
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
|
||||||
|
# MFI
|
||||||
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
|
||||||
|
# Minus Directional Indicator / Movement
|
||||||
|
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
|
|
||||||
|
# Plus Directional Indicator / Movement
|
||||||
|
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||||
|
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# ROC
|
||||||
|
dataframe['roc'] = ta.ROC(dataframe)
|
||||||
|
"""
|
||||||
|
# RSI
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
|
||||||
|
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||||
|
dataframe['fisher_rsi'] = fishers_inverse(dataframe['rsi'])
|
||||||
|
|
||||||
|
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||||
|
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||||
|
|
||||||
|
# Stoch
|
||||||
|
stoch = ta.STOCH(dataframe)
|
||||||
|
dataframe['slowd'] = stoch['slowd']
|
||||||
|
dataframe['slowk'] = stoch['slowk']
|
||||||
|
|
||||||
|
# Stoch fast
|
||||||
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
|
dataframe['fastk'] = stoch_fast['fastk']
|
||||||
|
"""
|
||||||
|
# Stoch RSI
|
||||||
|
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||||
|
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||||
|
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Overlap Studies
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
# Previous Bollinger bands
|
||||||
|
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
||||||
|
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
||||||
|
# and use middle band instead.
|
||||||
|
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||||
|
|
||||||
|
# Bollinger bands
|
||||||
|
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']
|
||||||
|
|
||||||
|
# EMA - Exponential Moving Average
|
||||||
|
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
|
||||||
|
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||||
|
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||||
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
|
||||||
|
# SAR Parabol
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
|
# SMA - Simple Moving Average
|
||||||
|
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||||
|
|
||||||
|
# TEMA - Triple Exponential Moving Average
|
||||||
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
|
|
||||||
|
# Cycle Indicator
|
||||||
|
# ------------------------------------
|
||||||
|
# Hilbert Transform Indicator - SineWave
|
||||||
|
hilbert = ta.HT_SINE(dataframe)
|
||||||
|
dataframe['htsine'] = hilbert['sine']
|
||||||
|
dataframe['htleadsine'] = hilbert['leadsine']
|
||||||
|
|
||||||
|
# Pattern Recognition - Bullish candlestick patterns
|
||||||
|
# ------------------------------------
|
||||||
|
"""
|
||||||
|
# Hammer: values [0, 100]
|
||||||
|
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||||
|
# Inverted Hammer: values [0, 100]
|
||||||
|
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||||
|
# Dragonfly Doji: values [0, 100]
|
||||||
|
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||||
|
# Piercing Line: values [0, 100]
|
||||||
|
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||||
|
# Morningstar: values [0, 100]
|
||||||
|
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||||
|
# Three White Soldiers: values [0, 100]
|
||||||
|
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pattern Recognition - Bearish candlestick patterns
|
||||||
|
# ------------------------------------
|
||||||
|
"""
|
||||||
|
# Hanging Man: values [0, 100]
|
||||||
|
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||||
|
# Shooting Star: values [0, 100]
|
||||||
|
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||||
|
# Gravestone Doji: values [0, 100]
|
||||||
|
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||||
|
# Dark Cloud Cover: values [0, 100]
|
||||||
|
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||||
|
# Evening Doji Star: values [0, 100]
|
||||||
|
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||||
|
# Evening Star: values [0, 100]
|
||||||
|
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||||
|
# ------------------------------------
|
||||||
|
"""
|
||||||
|
# Three Line Strike: values [0, -100, 100]
|
||||||
|
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||||
|
# Spinning Top: values [0, -100, 100]
|
||||||
|
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||||
|
# Engulfing: values [0, -100, 100]
|
||||||
|
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||||
|
# Harami: values [0, -100, 100]
|
||||||
|
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||||
|
# Three Outside Up/Down: values [0, -100, 100]
|
||||||
|
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||||
|
# Three Inside Up/Down: values [0, -100, 100]
|
||||||
|
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Chart type
|
||||||
|
# ------------------------------------
|
||||||
|
# Heikinashi stategy
|
||||||
|
heikinashi = qtpylib.heikinashi(dataframe)
|
||||||
|
dataframe['ha_open'] = heikinashi['open']
|
||||||
|
dataframe['ha_close'] = heikinashi['close']
|
||||||
|
dataframe['ha_high'] = heikinashi['high']
|
||||||
|
dataframe['ha_low'] = heikinashi['low']
|
||||||
|
|
||||||
|
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['rsi'] < 35) &
|
||||||
|
(dataframe['fastd'] < 35) &
|
||||||
|
(dataframe['adx'] > 30) &
|
||||||
|
(dataframe['plus_di'] > 0.5)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 65) &
|
||||||
|
(dataframe['plus_di'] > 0.5)
|
||||||
|
),
|
||||||
|
'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[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
||||||
|
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||||
|
) &
|
||||||
|
(dataframe['adx'] > 10) &
|
||||||
|
(dataframe['minus_di'] > 0)
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['minus_di'] > 0.5)
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
44
freqtrade/strategy/interface.py
Normal file
44
freqtrade/strategy/interface.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
IStrategy interface
|
||||||
|
This module defines the interface to apply for strategies
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
|
||||||
|
class IStrategy(ABC):
|
||||||
|
"""
|
||||||
|
Interface for freqtrade strategies
|
||||||
|
Defines the mandatory structure must follow any custom strategies
|
||||||
|
|
||||||
|
Attributes you can use:
|
||||||
|
minimal_roi -> Dict: Minimal ROI designed for the strategy
|
||||||
|
stoploss -> float: optimal stoploss designed for the strategy
|
||||||
|
ticker_interval -> int: value of the ticker interval to use for the strategy
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Populate indicators that will be used in the Buy and Sell strategy
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
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 sell column
|
||||||
|
"""
|
131
freqtrade/strategy/resolver.py
Normal file
131
freqtrade/strategy/resolver.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
# pragma pylint: disable=attribute-defined-outside-init
|
||||||
|
|
||||||
|
"""
|
||||||
|
This module load custom strategies
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
|
from typing import Optional, Dict, Type
|
||||||
|
|
||||||
|
from freqtrade import constants
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyResolver(object):
|
||||||
|
"""
|
||||||
|
This class contains all the logic to load custom strategy class
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ['strategy']
|
||||||
|
|
||||||
|
def __init__(self, config: Optional[Dict] = None) -> None:
|
||||||
|
"""
|
||||||
|
Load the custom class from config parameter
|
||||||
|
:param config: configuration dictionary or None
|
||||||
|
"""
|
||||||
|
config = config or {}
|
||||||
|
|
||||||
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||||
|
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||||
|
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
|
# Set attributes
|
||||||
|
# Check if we need to override configuration
|
||||||
|
if 'minimal_roi' in config:
|
||||||
|
self.strategy.minimal_roi = config['minimal_roi']
|
||||||
|
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||||
|
|
||||||
|
if 'stoploss' in config:
|
||||||
|
self.strategy.stoploss = config['stoploss']
|
||||||
|
logger.info(
|
||||||
|
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'ticker_interval' in config:
|
||||||
|
self.strategy.ticker_interval = config['ticker_interval']
|
||||||
|
logger.info(
|
||||||
|
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||||
|
config['ticker_interval']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort and apply type conversions
|
||||||
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
||||||
|
key=lambda t: t[0]))
|
||||||
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
|
self.strategy.ticker_interval = int(self.strategy.ticker_interval)
|
||||||
|
|
||||||
|
def _load_strategy(
|
||||||
|
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
|
||||||
|
"""
|
||||||
|
Search and loads the specified strategy.
|
||||||
|
:param strategy_name: name of the module to import
|
||||||
|
:param extra_dir: additional directory to search for the given strategy
|
||||||
|
:return: Strategy instance or None
|
||||||
|
"""
|
||||||
|
current_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
abs_paths = [
|
||||||
|
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
|
||||||
|
current_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
if extra_dir:
|
||||||
|
# Add extra strategy directory on top of search paths
|
||||||
|
abs_paths.insert(0, extra_dir)
|
||||||
|
|
||||||
|
for path in abs_paths:
|
||||||
|
strategy = self._search_strategy(path, strategy_name)
|
||||||
|
if strategy:
|
||||||
|
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
||||||
|
return strategy
|
||||||
|
|
||||||
|
raise ImportError(
|
||||||
|
"Impossible to load Strategy '{}'. This class does not exist"
|
||||||
|
" or contains Python code errors".format(strategy_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
|
||||||
|
"""
|
||||||
|
Returns a list of all possible strategies for the given module_path
|
||||||
|
:param module_path: absolute path to the module
|
||||||
|
:param strategy_name: Class name of the strategy
|
||||||
|
:return: Tuple with (name, class) or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generate spec based on absolute path
|
||||||
|
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
valid_strategies_gen = (
|
||||||
|
obj for name, obj in inspect.getmembers(module, inspect.isclass)
|
||||||
|
if strategy_name == name and IStrategy in obj.__bases__
|
||||||
|
)
|
||||||
|
return next(valid_strategies_gen, None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]:
|
||||||
|
"""
|
||||||
|
Search for the strategy_name in the given directory
|
||||||
|
:param directory: relative or absolute directory path
|
||||||
|
:return: name of the strategy class
|
||||||
|
"""
|
||||||
|
logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory)
|
||||||
|
for entry in os.listdir(directory):
|
||||||
|
# Only consider python files
|
||||||
|
if not entry.endswith('.py'):
|
||||||
|
logger.debug('Ignoring %s', entry)
|
||||||
|
continue
|
||||||
|
strategy = StrategyResolver._get_valid_strategies(
|
||||||
|
os.path.abspath(os.path.join(directory, entry)), strategy_name
|
||||||
|
)
|
||||||
|
if strategy:
|
||||||
|
return strategy()
|
||||||
|
return None
|
@ -1,16 +1,52 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from functools import reduce
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
from sqlalchemy import create_engine
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade.misc import CONF_SCHEMA
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade import constants
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
|
logging.getLogger('').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
def log_has(line, logs):
|
||||||
|
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
|
||||||
|
# and we want to match line against foobar in the tuple
|
||||||
|
return reduce(lambda a, b: a or b,
|
||||||
|
filter(lambda x: x[2] == line, logs),
|
||||||
|
False)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions for recurrent object patching
|
||||||
|
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
||||||
|
"""
|
||||||
|
This function patch _init_modules() to not call dependencies
|
||||||
|
:param mocker: a Mocker object to apply patches
|
||||||
|
:param config: Config to pass to the bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
||||||
|
|
||||||
|
return FreqtradeBot(config, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
def default_conf():
|
def default_conf():
|
||||||
""" Returns validated configuration suitable for most tests """
|
""" Returns validated configuration suitable for most tests """
|
||||||
configuration = {
|
configuration = {
|
||||||
@ -18,6 +54,7 @@ def default_conf():
|
|||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.001,
|
"stake_amount": 0.001,
|
||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
|
"ticker_interval": 5,
|
||||||
"dry_run": True,
|
"dry_run": True,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
@ -48,9 +85,10 @@ def default_conf():
|
|||||||
"token": "token",
|
"token": "token",
|
||||||
"chat_id": "0"
|
"chat_id": "0"
|
||||||
},
|
},
|
||||||
"initial_state": "running"
|
"initial_state": "running",
|
||||||
|
"loglevel": logging.DEBUG
|
||||||
}
|
}
|
||||||
validate(configuration, CONF_SCHEMA)
|
validate(configuration, constants.CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
@ -216,3 +254,178 @@ def ticker_history():
|
|||||||
"BV": 0.7039405
|
"BV": 0.7039405
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ticker_history_without_bv():
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"O": 8.794e-05,
|
||||||
|
"H": 8.948e-05,
|
||||||
|
"L": 8.794e-05,
|
||||||
|
"C": 8.88e-05,
|
||||||
|
"V": 991.09056638,
|
||||||
|
"T": "2017-11-26T08:50:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"O": 8.88e-05,
|
||||||
|
"H": 8.942e-05,
|
||||||
|
"L": 8.88e-05,
|
||||||
|
"C": 8.893e-05,
|
||||||
|
"V": 658.77935965,
|
||||||
|
"T": "2017-11-26T08:55:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"O": 8.891e-05,
|
||||||
|
"H": 8.893e-05,
|
||||||
|
"L": 8.875e-05,
|
||||||
|
"C": 8.877e-05,
|
||||||
|
"V": 7920.73570705,
|
||||||
|
"T": "2017-11-26T09:00:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# FIX: Perhaps change result fixture to use BTC_UNITEST instead?
|
||||||
|
@pytest.fixture
|
||||||
|
def result():
|
||||||
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
|
# FIX:
|
||||||
|
# Create an fixture/function
|
||||||
|
# that inserts a trade of some type and open-status
|
||||||
|
# return the open-order-id
|
||||||
|
# See tests in rpc/main that could use this
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def get_market_summaries_data():
|
||||||
|
"""
|
||||||
|
This fixture is a real result from exchange.get_market_summaries() but reduced to only
|
||||||
|
8 entries. 4 BTC, 4 USTD
|
||||||
|
:return: JSON market summaries
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'Ask': 1.316e-05,
|
||||||
|
'BaseVolume': 5.72599471,
|
||||||
|
'Bid': 1.3e-05,
|
||||||
|
'Created': '2014-04-14T00:00:00',
|
||||||
|
'High': 1.414e-05,
|
||||||
|
'Last': 1.298e-05,
|
||||||
|
'Low': 1.282e-05,
|
||||||
|
'MarketName': 'BTC-XWC',
|
||||||
|
'OpenBuyOrders': 2000,
|
||||||
|
'OpenSellOrders': 1484,
|
||||||
|
'PrevDay': 1.376e-05,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:40.493',
|
||||||
|
'Volume': 424041.21418375
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.00627051,
|
||||||
|
'BaseVolume': 93.23302388,
|
||||||
|
'Bid': 0.00618192,
|
||||||
|
'Created': '2016-10-20T04:48:30.387',
|
||||||
|
'High': 0.00669897,
|
||||||
|
'Last': 0.00618192,
|
||||||
|
'Low': 0.006,
|
||||||
|
'MarketName': 'BTC-XZC',
|
||||||
|
'OpenBuyOrders': 343,
|
||||||
|
'OpenSellOrders': 2037,
|
||||||
|
'PrevDay': 0.00668229,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.383',
|
||||||
|
'Volume': 14863.60730702
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.01137247,
|
||||||
|
'BaseVolume': 383.55922657,
|
||||||
|
'Bid': 0.01136006,
|
||||||
|
'Created': '2016-11-15T20:29:59.73',
|
||||||
|
'High': 0.012,
|
||||||
|
'Last': 0.01137247,
|
||||||
|
'Low': 0.01119883,
|
||||||
|
'MarketName': 'BTC-ZCL',
|
||||||
|
'OpenBuyOrders': 1332,
|
||||||
|
'OpenSellOrders': 5317,
|
||||||
|
'PrevDay': 0.01179603,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.773',
|
||||||
|
'Volume': 33308.07358285
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.04155821,
|
||||||
|
'BaseVolume': 274.75369074,
|
||||||
|
'Bid': 0.04130002,
|
||||||
|
'Created': '2016-10-28T17:13:10.833',
|
||||||
|
'High': 0.04354429,
|
||||||
|
'Last': 0.041585,
|
||||||
|
'Low': 0.0413,
|
||||||
|
'MarketName': 'BTC-ZEC',
|
||||||
|
'OpenBuyOrders': 863,
|
||||||
|
'OpenSellOrders': 5579,
|
||||||
|
'PrevDay': 0.0429,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.21',
|
||||||
|
'Volume': 6479.84033259
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 210.99999999,
|
||||||
|
'BaseVolume': 615132.70989532,
|
||||||
|
'Bid': 210.05503736,
|
||||||
|
'Created': '2017-07-21T01:08:49.397',
|
||||||
|
'High': 257.396,
|
||||||
|
'Last': 211.0,
|
||||||
|
'Low': 209.05333589,
|
||||||
|
'MarketName': 'USDT-XMR',
|
||||||
|
'OpenBuyOrders': 180,
|
||||||
|
'OpenSellOrders': 1203,
|
||||||
|
'PrevDay': 247.93528899,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:43.117',
|
||||||
|
'Volume': 2688.17410793
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.79589979,
|
||||||
|
'BaseVolume': 9349557.01853031,
|
||||||
|
'Bid': 0.789226,
|
||||||
|
'Created': '2017-07-14T17:10:10.737',
|
||||||
|
'High': 0.977,
|
||||||
|
'Last': 0.79589979,
|
||||||
|
'Low': 0.781,
|
||||||
|
'MarketName': 'USDT-XRP',
|
||||||
|
'OpenBuyOrders': 1075,
|
||||||
|
'OpenSellOrders': 6508,
|
||||||
|
'PrevDay': 0.93300218,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.383',
|
||||||
|
'Volume': 10801663.00788851
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 0.05154982,
|
||||||
|
'BaseVolume': 2311087.71232136,
|
||||||
|
'Bid': 0.05040107,
|
||||||
|
'Created': '2017-12-29T19:29:18.357',
|
||||||
|
'High': 0.06668561,
|
||||||
|
'Last': 0.0508,
|
||||||
|
'Low': 0.05006731,
|
||||||
|
'MarketName': 'USDT-XVG',
|
||||||
|
'OpenBuyOrders': 655,
|
||||||
|
'OpenSellOrders': 5544,
|
||||||
|
'PrevDay': 0.0627,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:41.507',
|
||||||
|
'Volume': 40031424.2152716
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Ask': 332.65500022,
|
||||||
|
'BaseVolume': 562911.87455665,
|
||||||
|
'Bid': 330.00000001,
|
||||||
|
'Created': '2017-07-14T17:10:10.673',
|
||||||
|
'High': 401.59999999,
|
||||||
|
'Last': 332.65500019,
|
||||||
|
'Low': 330.0,
|
||||||
|
'MarketName': 'USDT-ZEC',
|
||||||
|
'OpenBuyOrders': 161,
|
||||||
|
'OpenSellOrders': 1731,
|
||||||
|
'PrevDay': 391.42,
|
||||||
|
'TimeStamp': '2018-02-05T01:32:42.947',
|
||||||
|
'Volume': 1571.09647946
|
||||||
|
}
|
||||||
|
]
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
from unittest.mock import MagicMock
|
# pragma pylint: disable=protected-access
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from random import randint
|
|
||||||
import logging
|
import logging
|
||||||
import pytest
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
import freqtrade.exchange as exchange
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
||||||
get_ticker, cancel_order, get_name, get_fee
|
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
API_INIT = False
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_init_api(conf, mocker, force=False):
|
||||||
|
global API_INIT
|
||||||
|
if force or not API_INIT:
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs',
|
||||||
|
side_effect=lambda s: True)
|
||||||
|
init(config=conf)
|
||||||
|
API_INIT = True
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
caplog.set_level(logging.INFO)
|
||||||
side_effect=lambda s: True)
|
maybe_init_api(default_conf, mocker, True)
|
||||||
init(config=default_conf)
|
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
||||||
assert ('freqtrade.exchange',
|
|
||||||
logging.INFO,
|
|
||||||
'Instance is running with dry_run enabled'
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_exception(default_conf, mocker):
|
def test_init_exception(default_conf):
|
||||||
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
@ -60,16 +71,15 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.get_markets = MagicMock(side_effect=RequestException())
|
api_mock.get_markets = MagicMock(side_effect=RequestException())
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
|
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
assert ('freqtrade.exchange',
|
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
|
||||||
logging.WARNING,
|
caplog.record_tuples)
|
||||||
'Unable to validate pairs (assuming they are correct). Reason: '
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_buy_dry_run(default_conf, mocker):
|
def test_buy_dry_run(default_conf, mocker):
|
||||||
@ -159,8 +169,10 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
assert get_balances()[0]['Pending'] == 0.0
|
assert get_balances()[0]['Pending'] == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_get_ticker(mocker, ticker):
|
# This test is somewhat redundant with
|
||||||
|
# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker
|
||||||
|
def test_get_ticker(default_conf, mocker):
|
||||||
|
maybe_init_api(default_conf, mocker)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
|
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
|
||||||
api_mock.get_ticker = MagicMock(return_value=tick)
|
api_mock.get_ticker = MagicMock(return_value=tick)
|
||||||
@ -177,6 +189,7 @@ def test_get_ticker(mocker, ticker):
|
|||||||
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
|
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
|
||||||
|
|
||||||
# if not caching the result we should get the same ticker
|
# if not caching the result we should get the same ticker
|
||||||
|
# if not fetching a new result we should get the cached ticker
|
||||||
ticker = get_ticker(pair='BTC_ETH', refresh=False)
|
ticker = get_ticker(pair='BTC_ETH', refresh=False)
|
||||||
assert ticker['bid'] == 0.00001098
|
assert ticker['bid'] == 0.00001098
|
||||||
assert ticker['ask'] == 0.00001099
|
assert ticker['ask'] == 0.00001099
|
||||||
@ -187,6 +200,26 @@ def test_get_ticker(mocker, ticker):
|
|||||||
assert ticker['ask'] == 1
|
assert ticker['ask'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ticker_history(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
tick = 123
|
||||||
|
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
# retrieve original ticker
|
||||||
|
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
||||||
|
assert ticks == 123
|
||||||
|
|
||||||
|
# change the ticker
|
||||||
|
tick = 999
|
||||||
|
api_mock.get_ticker_history = MagicMock(return_value=tick)
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
# ensure caching will still return the original ticker
|
||||||
|
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
|
||||||
|
assert ticks == 123
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_order_dry_run(default_conf, mocker):
|
def test_cancel_order_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
@ -194,6 +227,33 @@ def test_cancel_order_dry_run(default_conf, mocker):
|
|||||||
assert cancel_order(order_id='123') is None
|
assert cancel_order(order_id='123') is None
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure that if not dry_run, we should call API
|
||||||
|
def test_cancel_order(default_conf, mocker):
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.cancel_order = MagicMock(return_value=123)
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
assert cancel_order(order_id='_') == 123
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_order(default_conf, mocker):
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
order = MagicMock()
|
||||||
|
order.myid = 123
|
||||||
|
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||||
|
print(exchange.get_order('X'))
|
||||||
|
assert exchange.get_order('X').myid == 123
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_order = MagicMock(return_value=456)
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
assert exchange.get_order('X') == 456
|
||||||
|
|
||||||
|
|
||||||
def test_get_name(default_conf, mocker):
|
def test_get_name(default_conf, mocker):
|
||||||
mocker.patch('freqtrade.exchange.validate_pairs',
|
mocker.patch('freqtrade.exchange.validate_pairs',
|
||||||
side_effect=lambda s: True)
|
side_effect=lambda s: True)
|
||||||
@ -209,3 +269,18 @@ def test_get_fee(default_conf, mocker):
|
|||||||
init(default_conf)
|
init(default_conf)
|
||||||
|
|
||||||
assert get_fee() == 0.0025
|
assert get_fee() == 0.0025
|
||||||
|
|
||||||
|
|
||||||
|
def test_exchange_misc(mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
exchange.get_markets()
|
||||||
|
assert api_mock.get_markets.call_count == 1
|
||||||
|
exchange.get_market_summaries()
|
||||||
|
assert api_mock.get_market_summaries.call_count == 1
|
||||||
|
api_mock.name = 123
|
||||||
|
assert exchange.get_name() == 123
|
||||||
|
api_mock.fee = 456
|
||||||
|
assert exchange.get_fee() == 456
|
||||||
|
exchange.get_wallet_health()
|
||||||
|
assert api_mock.get_wallet_health.call_count == 1
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from requests.exceptions import ContentDecodingError
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
|
||||||
import freqtrade.exchange.bittrex as btx
|
import freqtrade.exchange.bittrex as btx
|
||||||
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
|
||||||
|
|
||||||
# Eat this flake8
|
# Eat this flake8
|
||||||
@ -88,8 +89,7 @@ class FakeBittrex():
|
|||||||
'PricePerUnit': 1,
|
'PricePerUnit': 1,
|
||||||
'Quantity': 1,
|
'Quantity': 1,
|
||||||
'QuantityRemaining': 1,
|
'QuantityRemaining': 1,
|
||||||
'Closed': True
|
'Closed': True},
|
||||||
},
|
|
||||||
'message': 'lost'}
|
'message': 'lost'}
|
||||||
|
|
||||||
def fake_cancel_order(self, uuid):
|
def fake_cancel_order(self, uuid):
|
||||||
@ -143,7 +143,7 @@ def test_exchange_bittrex_fee():
|
|||||||
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
|
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_buy_good(mocker):
|
def test_exchange_bittrex_buy_good():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
uuid = wb.buy('BTC_ETH', 1, 1)
|
uuid = wb.buy('BTC_ETH', 1, 1)
|
||||||
@ -154,7 +154,7 @@ def test_exchange_bittrex_buy_good(mocker):
|
|||||||
wb.buy('BAD', 1, 1)
|
wb.buy('BAD', 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_sell_good(mocker):
|
def test_exchange_bittrex_sell_good():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
uuid = wb.sell('BTC_ETH', 1, 1)
|
uuid = wb.sell('BTC_ETH', 1, 1)
|
||||||
@ -165,7 +165,7 @@ def test_exchange_bittrex_sell_good(mocker):
|
|||||||
uuid = wb.sell('BAD', 1, 1)
|
uuid = wb.sell('BAD', 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_balance(mocker):
|
def test_exchange_bittrex_get_balance():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
bal = wb.get_balance('BTC_ETH')
|
bal = wb.get_balance('BTC_ETH')
|
||||||
@ -211,28 +211,40 @@ def test_exchange_bittrex_get_ticker():
|
|||||||
def test_exchange_bittrex_get_ticker_bad():
|
def test_exchange_bittrex_get_ticker_bad():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
fb.result = {'success': True,
|
fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result
|
||||||
'result': {'Bid': 1}} # incomplete result
|
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'):
|
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
||||||
wb.get_ticker('BTC_ETH')
|
wb.get_ticker('BTC_ETH')
|
||||||
fb.result = {'success': False,
|
fb.result = {'success': False, 'message': 'gone bad'}
|
||||||
'message': 'gone bad'
|
|
||||||
}
|
|
||||||
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
||||||
wb.get_ticker('BTC_ETH')
|
wb.get_ticker('BTC_ETH')
|
||||||
|
|
||||||
|
fb.result = {'success': True, 'result': {}} # incomplete result
|
||||||
|
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
||||||
|
wb.get_ticker('BTC_ETH')
|
||||||
|
fb.result = {'success': False, 'message': 'gone bad'}
|
||||||
|
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
|
||||||
|
wb.get_ticker('BTC_ETH')
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker_history_one():
|
fb.result = {'success': True,
|
||||||
|
'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result
|
||||||
|
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
|
||||||
|
wb.get_ticker('BTC_ETH')
|
||||||
|
|
||||||
|
|
||||||
|
def test_exchange_bittrex_get_ticker_history_intervals():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
FakeBittrex()
|
FakeBittrex()
|
||||||
assert wb.get_ticker_history('BTC_ETH', 1)
|
for tick_interval in [1, 5, 30, 60, 1440]:
|
||||||
|
assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
|
||||||
|
wb.get_ticker_history('BTC_ETH', tick_interval))
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_bittrex_get_ticker_history():
|
def test_exchange_bittrex_get_ticker_history():
|
||||||
wb = make_wrap_bittrex()
|
wb = make_wrap_bittrex()
|
||||||
fb = FakeBittrex()
|
fb = FakeBittrex()
|
||||||
assert wb.get_ticker_history('BTC_ETH', 5)
|
assert wb.get_ticker_history('BTC_ETH', 5)
|
||||||
with pytest.raises(ValueError, match=r'.*Cannot parse tick_interval.*'):
|
with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'):
|
||||||
wb.get_ticker_history('BTC_ETH', 2)
|
wb.get_ticker_history('BTC_ETH', 2)
|
||||||
|
|
||||||
fb.success = False
|
fb.success = False
|
||||||
@ -240,7 +252,7 @@ def test_exchange_bittrex_get_ticker_history():
|
|||||||
wb.get_ticker_history('BTC_ETH', 5)
|
wb.get_ticker_history('BTC_ETH', 5)
|
||||||
|
|
||||||
fb.success = True
|
fb.success = True
|
||||||
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex.*'):
|
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'):
|
||||||
fb.result = {'bad': 0}
|
fb.result = {'bad': 0}
|
||||||
wb.get_ticker_history('BTC_ETH', 5)
|
wb.get_ticker_history('BTC_ETH', 5)
|
||||||
|
|
||||||
|
@ -1,74 +1,40 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
|
||||||
|
|
||||||
import logging
|
import json
|
||||||
import math
|
import math
|
||||||
import pandas as pd
|
import random
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import List
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from freqtrade import exchange, optimize
|
|
||||||
from freqtrade.exchange import Bittrex
|
import numpy as np
|
||||||
from freqtrade.optimize import preprocess
|
import pandas as pd
|
||||||
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
|
from arrow import Arrow
|
||||||
import freqtrade.optimize.backtesting as backtesting
|
|
||||||
|
from freqtrade import optimize
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
||||||
|
from freqtrade.tests.conftest import default_conf, log_has
|
||||||
|
|
||||||
|
# Avoid to reinit the same object again and again
|
||||||
|
_BACKTESTING = Backtesting(default_conf())
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table():
|
def get_args(args) -> List[str]:
|
||||||
results = pd.DataFrame(
|
return Arguments(args, '').get_parsed_arg()
|
||||||
{
|
|
||||||
'currency': ['BTC_ETH', 'BTC_ETH'],
|
|
||||||
'profit_percent': [0.1, 0.2],
|
|
||||||
'profit_BTC': [0.2, 0.4],
|
|
||||||
'duration': [10, 30],
|
|
||||||
'profit': [2, 0],
|
|
||||||
'loss': [0, 0]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5))
|
|
||||||
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
|
|
||||||
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
|
|
||||||
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
|
|
||||||
'BTC_ETH 2 15.00 0.60000000 100.0 2 0\n' # noqa
|
|
||||||
'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe():
|
def trim_dictlist(dict_list, num):
|
||||||
data = preprocess(optimize.load_data(
|
|
||||||
None, ticker_interval=1, pairs=['BTC_UNITEST']))
|
|
||||||
min_date, max_date = get_timeframe(data)
|
|
||||||
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
|
||||||
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
|
||||||
results = backtest(default_conf['stake_amount'],
|
|
||||||
optimize.preprocess(data), 10, True)
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
# Run a backtesting for an exiting 5min ticker_interval
|
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
|
||||||
results = backtest(default_conf['stake_amount'],
|
|
||||||
optimize.preprocess(data), 1, True)
|
|
||||||
assert not results.empty
|
|
||||||
|
|
||||||
|
|
||||||
def trim_dictlist(dl, num):
|
|
||||||
new = {}
|
new = {}
|
||||||
for pair, pair_data in dl.items():
|
for pair, pair_data in dict_list.items():
|
||||||
new[pair] = pair_data[num:]
|
new[pair] = pair_data[num:]
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
def load_data_test(what):
|
def load_data_test(what):
|
||||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
timerange = ((None, 'line'), None, -100)
|
||||||
data = trim_dictlist(data, -100)
|
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
|
||||||
pair = data['BTC_UNITEST']
|
pair = data['BTC_UNITEST']
|
||||||
datalen = len(pair)
|
datalen = len(pair)
|
||||||
# Depending on the what parameter we now adjust the
|
# Depending on the what parameter we now adjust the
|
||||||
@ -109,31 +75,404 @@ def load_data_test(what):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def simple_backtest(config, contour, num_results):
|
def simple_backtest(config, contour, num_results) -> None:
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
data = load_data_test(contour)
|
data = load_data_test(contour)
|
||||||
processed = optimize.preprocess(data)
|
processed = backtesting.tickerdata_to_dataframe(data)
|
||||||
assert isinstance(processed, dict)
|
assert isinstance(processed, dict)
|
||||||
results = backtest(config['stake_amount'], processed, 1, True)
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': config['stake_amount'],
|
||||||
|
'processed': processed,
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
assert len(results) == num_results
|
assert len(results) == num_results
|
||||||
|
|
||||||
|
|
||||||
# Test backtest on offline data
|
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
|
||||||
# loaded by freqdata/optimize/__init__.py::load_data()
|
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
pairdata = {'BTC_UNITEST': tickerdata}
|
||||||
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
def test_backtest2(default_conf, mocker):
|
# use for mock freqtrade.exchange.get_ticker_history'
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
|
ticks = trim_dictlist(ticks, -200)
|
||||||
|
return ticks[pair]
|
||||||
|
|
||||||
|
|
||||||
|
# FIX: fixturize this?
|
||||||
|
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
|
||||||
|
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
|
||||||
|
data = trim_dictlist(data, -200)
|
||||||
|
return {
|
||||||
|
'stake_amount': conf['stake_amount'],
|
||||||
|
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 10,
|
||||||
|
'realistic': True,
|
||||||
|
'record': record
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _trend(signals, buy_value, sell_value):
|
||||||
|
n = len(signals['low'])
|
||||||
|
buy = np.zeros(n)
|
||||||
|
sell = np.zeros(n)
|
||||||
|
for i in range(0, len(signals['buy'])):
|
||||||
|
if random.random() > 0.5: # Both buy and sell signals at same timeframe
|
||||||
|
buy[i] = buy_value
|
||||||
|
sell[i] = sell_value
|
||||||
|
signals['buy'] = buy
|
||||||
|
signals['sell'] = sell
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
def _trend_alternate(dataframe=None):
|
||||||
|
signals = dataframe
|
||||||
|
low = signals['low']
|
||||||
|
n = len(low)
|
||||||
|
buy = np.zeros(n)
|
||||||
|
sell = np.zeros(n)
|
||||||
|
for i in range(0, len(buy)):
|
||||||
|
if i % 2 == 0:
|
||||||
|
buy[i] = 1
|
||||||
|
else:
|
||||||
|
sell[i] = 1
|
||||||
|
signals['buy'] = buy
|
||||||
|
signals['sell'] = sell
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def _run_backtest_1(fun, backtest_conf):
|
||||||
|
# strategy is a global (hidden as a singleton), so we
|
||||||
|
# emulate strategy being pure, by override/restore here
|
||||||
|
# if we dont do this, the override in strategy will carry over
|
||||||
|
# to other tests
|
||||||
|
old_buy = _BACKTESTING.populate_buy_trend
|
||||||
|
old_sell = _BACKTESTING.populate_sell_trend
|
||||||
|
_BACKTESTING.populate_buy_trend = fun # Override
|
||||||
|
_BACKTESTING.populate_sell_trend = fun # Override
|
||||||
|
results = _BACKTESTING.backtest(backtest_conf)
|
||||||
|
_BACKTESTING.populate_buy_trend = old_buy # restore override
|
||||||
|
_BACKTESTING.populate_sell_trend = old_sell # restore override
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'live' not in config
|
||||||
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation' not in config
|
||||||
|
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'export' not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--realistic-simulation',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--export', '/bar/foo'
|
||||||
|
]
|
||||||
|
|
||||||
|
config = setup_configuration(get_args(args))
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1 ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'live' in config
|
||||||
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation'in config
|
||||||
|
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs'in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'export' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test start() function
|
||||||
|
"""
|
||||||
|
start_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
|
assert log_has(
|
||||||
|
'Starting freqtrade in Backtesting mode',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting__init__(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.__init__() method
|
||||||
|
"""
|
||||||
|
init_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
|
||||||
|
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
assert backtesting.config == default_conf
|
||||||
|
assert backtesting.analyze is None
|
||||||
|
assert backtesting.ticker_interval is None
|
||||||
|
assert backtesting.tickerdata_to_dataframe is None
|
||||||
|
assert backtesting.populate_buy_trend is None
|
||||||
|
assert backtesting.populate_sell_trend is None
|
||||||
|
assert init_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_init(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting._init() method
|
||||||
|
"""
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
assert backtesting.config == default_conf
|
||||||
|
assert isinstance(backtesting.analyze, Analyze)
|
||||||
|
assert backtesting.ticker_interval == 5
|
||||||
|
assert callable(backtesting.tickerdata_to_dataframe)
|
||||||
|
assert callable(backtesting.populate_buy_trend)
|
||||||
|
assert callable(backtesting.populate_sell_trend)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.tickerdata_to_dataframe() method
|
||||||
|
"""
|
||||||
|
|
||||||
|
timerange = ((None, 'line'), None, -100)
|
||||||
|
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert len(data['BTC_UNITEST']) == 100
|
||||||
|
|
||||||
|
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timeframe() -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.get_timeframe() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
data = backtesting.tickerdata_to_dataframe(
|
||||||
|
optimize.load_data(
|
||||||
|
None,
|
||||||
|
ticker_interval=1,
|
||||||
|
pairs=['BTC_UNITEST']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
min_date, max_date = backtesting.get_timeframe(data)
|
||||||
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
|
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table():
|
||||||
|
"""
|
||||||
|
Test Backtesting.generate_text_table() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'currency': ['BTC_ETH', 'BTC_ETH'],
|
||||||
|
'profit_percent': [0.1, 0.2],
|
||||||
|
'profit_BTC': [0.2, 0.4],
|
||||||
|
'duration': [10, 30],
|
||||||
|
'profit': [2, 0],
|
||||||
|
'loss': [0, 0]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'pair buy count avg profit % '
|
||||||
|
'total profit BTC avg duration profit loss\n'
|
||||||
|
'------- ----------- -------------- '
|
||||||
|
'------------------ -------------- -------- ------\n'
|
||||||
|
'BTC_ETH 2 15.00 '
|
||||||
|
'0.60000000 20.0 2 0\n'
|
||||||
|
'TOTAL 2 15.00 '
|
||||||
|
'0.60000000 20.0 2 0'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.start() method
|
||||||
|
"""
|
||||||
|
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', mocked_load_data)
|
||||||
|
mocker.patch('freqtrade.exchange.get_ticker_history')
|
||||||
|
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'] = ['BTC_UNITEST']
|
||||||
|
conf['ticker_interval'] = 1
|
||||||
|
conf['live'] = False
|
||||||
|
conf['datadir'] = None
|
||||||
|
conf['export'] = None
|
||||||
|
conf['timerange'] = '-100'
|
||||||
|
|
||||||
|
backtesting = Backtesting(conf)
|
||||||
|
backtesting.start()
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
exists = [
|
||||||
|
'Using local backtesting data (using whitelist in given config) ...',
|
||||||
|
'Using stake_currency: BTC ...',
|
||||||
|
'Using stake_amount: 0.001 ...',
|
||||||
|
'Measuring data from 2017-11-14T21:17:00+00:00 '
|
||||||
|
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||||
|
]
|
||||||
|
for line in exists:
|
||||||
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.backtest() method
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||||
results = backtest(default_conf['stake_amount'],
|
data = trim_dictlist(data, -200)
|
||||||
optimize.preprocess(data), 10, True)
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 10,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_processed(default_conf, mocker):
|
def test_backtest_1min_ticker_interval(default_conf) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
|
Test Backtesting.backtest() method with 1 min ticker
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
|
# Run a backtesting for an exiting 5min ticker_interval
|
||||||
|
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||||
|
data = trim_dictlist(data, -200)
|
||||||
|
results = backtesting.backtest(
|
||||||
|
{
|
||||||
|
'stake_amount': default_conf['stake_amount'],
|
||||||
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
|
'max_open_trades': 1,
|
||||||
|
'realistic': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_processed() -> None:
|
||||||
|
"""
|
||||||
|
Test Backtesting.backtest() method with offline data
|
||||||
|
"""
|
||||||
|
backtesting = _BACKTESTING
|
||||||
|
|
||||||
dict_of_tickerrows = load_data_test('raise')
|
dict_of_tickerrows = load_data_test('raise')
|
||||||
dataframes = optimize.preprocess(dict_of_tickerrows)
|
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
|
||||||
dataframe = dataframes['BTC_UNITEST']
|
dataframe = dataframes['BTC_UNITEST']
|
||||||
cols = dataframe.columns
|
cols = dataframe.columns
|
||||||
# assert the dataframe got some of the indicator columns
|
# assert the dataframe got some of the indicator columns
|
||||||
@ -142,36 +481,129 @@ def test_processed(default_conf, mocker):
|
|||||||
assert col in cols
|
assert col in cols
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_pricecontours(default_conf, mocker):
|
def test_backtest_pricecontours(default_conf) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
||||||
for [contour, numres] in tests:
|
for [contour, numres] in tests:
|
||||||
simple_backtest(default_conf, contour, numres)
|
simple_backtest(default_conf, contour, numres)
|
||||||
|
|
||||||
|
|
||||||
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False):
|
# Test backtest using offline data (testdata directory)
|
||||||
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1)
|
def test_backtest_ticks(default_conf):
|
||||||
pairdata = {'BTC_UNITEST': tickerdata}
|
ticks = [1, 5]
|
||||||
return trim_dictlist(pairdata, -100)
|
fun = _BACKTESTING.populate_buy_trend
|
||||||
|
for tick in ticks:
|
||||||
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_start(default_conf, mocker, caplog):
|
def test_backtest_clash_buy_sell(default_conf):
|
||||||
|
# Override the default buy trend function in our DefaultStrategy
|
||||||
|
def fun(dataframe=None):
|
||||||
|
buy_value = 1
|
||||||
|
sell_value = 1
|
||||||
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_only_sell(default_conf):
|
||||||
|
# Override the default buy trend function in our DefaultStrategy
|
||||||
|
def fun(dataframe=None):
|
||||||
|
buy_value = 0
|
||||||
|
sell_value = 1
|
||||||
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
|
backtest_conf = _make_backtest_conf(conf=default_conf)
|
||||||
|
results = _run_backtest_1(fun, backtest_conf)
|
||||||
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_alternate_buy_sell(default_conf):
|
||||||
|
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
|
||||||
|
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
||||||
|
assert len(results) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_record(default_conf, mocker):
|
||||||
|
names = []
|
||||||
|
records = []
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.backtesting.file_dump_json',
|
||||||
|
new=lambda n, r: (names.append(n), records.append(r))
|
||||||
|
)
|
||||||
|
backtest_conf = _make_backtest_conf(
|
||||||
|
conf=default_conf,
|
||||||
|
pair='BTC_UNITEST',
|
||||||
|
record="trades"
|
||||||
|
)
|
||||||
|
results = _run_backtest_1(_trend_alternate, backtest_conf)
|
||||||
|
assert len(results) == 3
|
||||||
|
# Assert file_dump_json was only called once
|
||||||
|
assert names == ['backtest-result.json']
|
||||||
|
records = records[0]
|
||||||
|
# Ensure records are of correct type
|
||||||
|
assert len(records) == 3
|
||||||
|
# ('BTC_UNITEST', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||||
|
# Below follows just a typecheck of the schema/type of trade-records
|
||||||
|
oix = None
|
||||||
|
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
|
||||||
|
assert pair == 'BTC_UNITEST'
|
||||||
|
isinstance(profit, float)
|
||||||
|
# FIX: buy/sell should be converted to ints
|
||||||
|
isinstance(date_buy, str)
|
||||||
|
isinstance(date_sell, str)
|
||||||
|
isinstance(buy_index, pd._libs.tslib.Timestamp)
|
||||||
|
if oix:
|
||||||
|
assert buy_index > oix
|
||||||
|
oix = buy_index
|
||||||
|
assert dur > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch('freqtrade.exchange.get_ticker_history',
|
||||||
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
|
new=lambda n, i: _load_pair_as_ticks(n, i))
|
||||||
mocker.patch.multiple('freqtrade.optimize',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
load_data=mocked_load_data)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
args = MagicMock()
|
args = MagicMock()
|
||||||
args.ticker_interval = 1
|
args.ticker_interval = 1
|
||||||
args.level = 10
|
args.level = 10
|
||||||
args.live = False
|
args.live = True
|
||||||
args.datadir = None
|
args.datadir = None
|
||||||
backtesting.start(args)
|
args.export = None
|
||||||
|
args.strategy = 'DefaultStrategy'
|
||||||
|
args.timerange = '-100' # needed due to MagicMock malleability
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--timerange', '-100'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = ['Using max_open_trades: 1 ...',
|
exists = [
|
||||||
'Using stake_amount: 0.001 ...',
|
'Parameter -i/--ticker-interval detected ...',
|
||||||
'Measuring data from 2017-11-14T21:17:00+00:00 up to 2017-11-14T22:59:00+00:00 ...']
|
'Using ticker_interval: 1 ...',
|
||||||
|
'Parameter -l/--live detected ...',
|
||||||
|
'Using max_open_trades: 1 ...',
|
||||||
|
'Parameter --timerange detected: -100 ..',
|
||||||
|
'Parameter --datadir detected: freqtrade/tests/testdata ...',
|
||||||
|
'Using stake_currency: BTC ...',
|
||||||
|
'Using stake_amount: 0.001 ...',
|
||||||
|
'Downloading data for all pairs in whitelist ...',
|
||||||
|
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
|
||||||
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert ('freqtrade.optimize.backtesting',
|
log_has(line, caplog.record_tuples)
|
||||||
logging.INFO,
|
|
||||||
line) in caplog.record_tuples
|
|
||||||
|
@ -1,112 +1,143 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
|
import json
|
||||||
log_results, save_trials, read_trials
|
import os
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
from freqtrade.optimize.hyperopt import Hyperopt, start
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
from freqtrade.tests.conftest import default_conf, log_has
|
||||||
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count():
|
# Avoid to reinit the same object again and again
|
||||||
correct = calculate_loss(1, TARGET_TRADES, 20)
|
_HYPEROPT = Hyperopt(default_conf())
|
||||||
over = calculate_loss(1, TARGET_TRADES + 100, 20)
|
|
||||||
under = calculate_loss(1, TARGET_TRADES - 100, 20)
|
|
||||||
assert over > correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_shorter_trades():
|
# Functions for recurrent object patching
|
||||||
shorter = calculate_loss(1, 100, 20)
|
def create_trials(mocker) -> None:
|
||||||
longer = calculate_loss(1, 100, 30)
|
|
||||||
assert shorter < longer
|
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_has_limited_profit():
|
|
||||||
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
|
|
||||||
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
|
|
||||||
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
|
|
||||||
assert over == correct
|
|
||||||
assert under > correct
|
|
||||||
|
|
||||||
|
|
||||||
def create_trials(mocker):
|
|
||||||
"""
|
"""
|
||||||
When creating trials, mock the hyperopt Trials so that *by default*
|
When creating trials, mock the hyperopt Trials so that *by default*
|
||||||
- we don't create any pickle'd files in the filesystem
|
- we don't create any pickle'd files in the filesystem
|
||||||
- we might have a pickle'd file so make sure that we return
|
- we might have a pickle'd file so make sure that we return
|
||||||
false when looking for it
|
false when looking for it
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
|
_HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
return_value='freqtrade/tests/optimize/ut_trials.pickle')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||||
return_value=False)
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.save_trials',
|
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||||
return_value=None)
|
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
|
|
||||||
return_value=None)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
|
|
||||||
return_value=True)
|
|
||||||
return mocker.Mock(
|
return mocker.Mock(
|
||||||
results=[{
|
results=[
|
||||||
'loss': 1,
|
{
|
||||||
'result': 'foo',
|
'loss': 1,
|
||||||
'status': 'ok'
|
'result': 'foo',
|
||||||
}],
|
'status': 'ok'
|
||||||
|
}
|
||||||
|
],
|
||||||
best_trial={'misc': {'vals': {'adx': 999}}}
|
best_trial={'misc': {'vals': {'adx': 999}}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_fmin(mocker):
|
# Unit tests
|
||||||
trials = create_trials(mocker)
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials)
|
"""
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted',
|
Test start() function
|
||||||
return_value=trials.results)
|
"""
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
start_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
|
))
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '5'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
start(args)
|
start(args)
|
||||||
|
|
||||||
mock_fmin.assert_called_once()
|
import pprint
|
||||||
|
pprint.pprint(caplog.record_tuples)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Starting freqtrade in Hyperopt mode',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_start_uses_mongotrials(mocker):
|
def test_loss_calculation_prefer_correct_trade_count() -> None:
|
||||||
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
|
"""
|
||||||
return_value=create_trials(mocker))
|
Test Hyperopt.calculate_loss()
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
"""
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
hyperopt = _HYPEROPT
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
|
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||||
start(args)
|
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
|
||||||
|
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
|
||||||
mock_mongotrials.assert_called_once()
|
assert over > correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_log_results_if_loss_improves(mocker):
|
def test_loss_calculation_prefer_shorter_trades() -> None:
|
||||||
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
"""
|
||||||
global CURRENT_BEST_LOSS
|
Test Hyperopt.calculate_loss()
|
||||||
CURRENT_BEST_LOSS = 2
|
"""
|
||||||
log_results({
|
hyperopt = _HYPEROPT
|
||||||
'loss': 1,
|
|
||||||
'current_tries': 1,
|
|
||||||
'total_tries': 2,
|
|
||||||
'result': 'foo'
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.assert_called_once()
|
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||||
|
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||||
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(mocker):
|
def test_loss_calculation_has_limited_profit() -> None:
|
||||||
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
hyperopt = _HYPEROPT
|
||||||
global CURRENT_BEST_LOSS
|
|
||||||
CURRENT_BEST_LOSS = 2
|
|
||||||
log_results({
|
|
||||||
'loss': 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
assert not logger.called
|
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||||
|
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
|
||||||
|
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
|
||||||
|
assert over == correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_best_results(mocker, caplog):
|
def test_log_results_if_loss_improves(capsys) -> None:
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt.current_best_loss = 2
|
||||||
|
hyperopt.log_results(
|
||||||
|
{
|
||||||
|
'loss': 1,
|
||||||
|
'current_tries': 1,
|
||||||
|
'total_tries': 2,
|
||||||
|
'result': 'foo'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert ' 1/2: foo. Loss 1.00000'in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_log_if_loss_does_not_improve(caplog) -> None:
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt.current_best_loss = 2
|
||||||
|
hyperopt.log_results(
|
||||||
|
{
|
||||||
|
'loss': 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert caplog.record_tuples == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
|
||||||
fmin_result = {
|
fmin_result = {
|
||||||
|
"macd_below_zero": 0,
|
||||||
"adx": 1,
|
"adx": 1,
|
||||||
"adx-value": 15.0,
|
"adx-value": 15.0,
|
||||||
"fastd": 1,
|
"fastd": 1,
|
||||||
@ -121,37 +152,73 @@ def test_fmin_best_results(mocker, caplog):
|
|||||||
"uptrend_short_ema": 0,
|
"uptrend_short_ema": 0,
|
||||||
"uptrend_sma": 0,
|
"uptrend_sma": 0,
|
||||||
"stoploss": -0.1,
|
"stoploss": -0.1,
|
||||||
|
"roi_t1": 1,
|
||||||
|
"roi_t2": 2,
|
||||||
|
"roi_t3": 3,
|
||||||
|
"roi_p1": 1,
|
||||||
|
"roi_p2": 2,
|
||||||
|
"roi_p3": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
conf = deepcopy(default_conf)
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
conf.update({'config': 'config.json.example'})
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
conf.update({'epochs': 1})
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example')
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
start(args)
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
|
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = create_trials(mocker)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
exists = [
|
exists = [
|
||||||
'Best parameters',
|
'Best parameters:',
|
||||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
||||||
|
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
|
||||||
'"green_candle": {\n "enabled": true\n },',
|
'"green_candle": {\n "enabled": true\n },',
|
||||||
|
'"macd_below_zero": {\n "enabled": false\n },',
|
||||||
'"mfi": {\n "enabled": false\n },',
|
'"mfi": {\n "enabled": false\n },',
|
||||||
'"trigger": {\n "type": "ao_cross_zero"\n },',
|
'"over_sar": {\n "enabled": false\n },',
|
||||||
'"stoploss": -0.1',
|
'"roi_p1": 1.0,',
|
||||||
|
'"roi_p2": 2.0,',
|
||||||
|
'"roi_p3": 3.0,',
|
||||||
|
'"roi_t1": 1.0,',
|
||||||
|
'"roi_t2": 2.0,',
|
||||||
|
'"roi_t3": 3.0,',
|
||||||
|
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
|
||||||
|
'"stoploss": -0.1,',
|
||||||
|
'"trigger": {\n "type": "faststoch10"\n },',
|
||||||
|
'"uptrend_long_ema": {\n "enabled": true\n },',
|
||||||
|
'"uptrend_short_ema": {\n "enabled": false\n },',
|
||||||
|
'"uptrend_sma": {\n "enabled": false\n }',
|
||||||
|
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
|
||||||
|
'Best Result:\nfoo'
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_throw_value_error(mocker, caplog):
|
def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
||||||
|
|
||||||
args = mocker.Mock(epochs=1, config='config.json.example')
|
conf = deepcopy(default_conf)
|
||||||
start(args)
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'epochs': 1})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = create_trials(mocker)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
exists = [
|
exists = [
|
||||||
'Best Result:',
|
'Best Result:',
|
||||||
@ -163,61 +230,305 @@ def test_fmin_throw_value_error(mocker, caplog):
|
|||||||
assert line in caplog.text
|
assert line in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_resuming_previous_hyperopt_results_succeeds(mocker):
|
def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
|
||||||
import freqtrade.optimize.hyperopt as hyperopt
|
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
|
|
||||||
return_value=trials)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
|
|
||||||
return_value=True)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.len',
|
|
||||||
return_value=len(trials.results))
|
|
||||||
mock_read = mocker.patch('freqtrade.optimize.hyperopt.read_trials',
|
|
||||||
return_value=trials)
|
|
||||||
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials',
|
|
||||||
return_value=None)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted',
|
|
||||||
return_value=trials.results)
|
|
||||||
mocker.patch('freqtrade.optimize.preprocess')
|
|
||||||
mocker.patch('freqtrade.optimize.load_data')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin',
|
|
||||||
return_value={})
|
|
||||||
args = mocker.Mock(epochs=1,
|
|
||||||
config='config.json.example',
|
|
||||||
mongodb=False)
|
|
||||||
|
|
||||||
start(args)
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'epochs': 1})
|
||||||
|
conf.update({'mongodb': False})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
|
||||||
|
mock_read = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
|
||||||
|
return_value=trials
|
||||||
|
)
|
||||||
|
mock_save = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
|
||||||
|
return_value=None
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
|
||||||
|
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = trials
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
|
||||||
mock_read.assert_called_once()
|
mock_read.assert_called_once()
|
||||||
mock_save.assert_called_once()
|
mock_save.assert_called_once()
|
||||||
|
|
||||||
current_tries = hyperopt._CURRENT_TRIES
|
current_tries = hyperopt.current_tries
|
||||||
total_tries = hyperopt.TOTAL_TRIES
|
total_tries = hyperopt.total_tries
|
||||||
|
|
||||||
assert current_tries == len(trials.results)
|
assert current_tries == len(trials.results)
|
||||||
assert total_tries == (current_tries + len(trials.results))
|
assert total_tries == (current_tries + len(trials.results))
|
||||||
|
|
||||||
|
|
||||||
def test_save_trials_saves_trials(mocker):
|
def test_save_trials_saves_trials(mocker, caplog) -> None:
|
||||||
|
create_trials(mocker)
|
||||||
|
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
||||||
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
|
||||||
|
|
||||||
|
hyperopt.save_trials()
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
mock_dump.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker)
|
||||||
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump',
|
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
||||||
return_value=None)
|
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
||||||
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
|
|
||||||
return_value='ut_trials.pickle')
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.open',
|
|
||||||
return_value=trials_path)
|
|
||||||
save_trials(trials, trials_path)
|
|
||||||
|
|
||||||
mock_dump.assert_called_once_with(trials, trials_path)
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt_trial = hyperopt.read_trials()
|
||||||
|
assert log_has(
|
||||||
def test_read_trials_returns_trials_file(mocker):
|
'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
|
||||||
trials = create_trials(mocker)
|
caplog.record_tuples
|
||||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load',
|
)
|
||||||
return_value=trials)
|
assert hyperopt_trial == trials
|
||||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open',
|
|
||||||
return_value=mock_load)
|
|
||||||
|
|
||||||
assert read_trials() == trials
|
|
||||||
mock_open.assert_called_once()
|
mock_open.assert_called_once()
|
||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_roi_table_generation() -> None:
|
||||||
|
params = {
|
||||||
|
'roi_t1': 5,
|
||||||
|
'roi_t2': 10,
|
||||||
|
'roi_t3': 15,
|
||||||
|
'roi_p1': 1,
|
||||||
|
'roi_p2': 2,
|
||||||
|
'roi_p3': 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_calls_fmin(mocker, default_conf) -> None:
|
||||||
|
trials = create_trials(mocker)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'epochs': 1})
|
||||||
|
conf.update({'mongodb': False})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.trials = trials
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_uses_mongotrials(mocker, 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)
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
|
hyperopt.start()
|
||||||
|
mock_mongotrials.assert_called_once()
|
||||||
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
# test log_trials_result
|
||||||
|
# test buy_strategy_generator def populate_buy_trend
|
||||||
|
# test optimizer if 'ro_t1' in params
|
||||||
|
|
||||||
|
def test_format_results():
|
||||||
|
"""
|
||||||
|
Test Hyperopt.format_results()
|
||||||
|
"""
|
||||||
|
trades = [
|
||||||
|
('BTC_ETH', 2, 2, 123),
|
||||||
|
('BTC_LTC', 1, 1, 123),
|
||||||
|
('BTC_XRP', -1, -2, -246)
|
||||||
|
]
|
||||||
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
x = Hyperopt.format_results(df)
|
||||||
|
assert x.find(' 66.67%')
|
||||||
|
|
||||||
|
|
||||||
|
def test_signal_handler(mocker):
|
||||||
|
"""
|
||||||
|
Test Hyperopt.signal_handler()
|
||||||
|
"""
|
||||||
|
m = MagicMock()
|
||||||
|
mocker.patch('sys.exit', m)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
|
||||||
|
|
||||||
|
hyperopt = _HYPEROPT
|
||||||
|
hyperopt.signal_handler(9, None)
|
||||||
|
assert m.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_space():
|
||||||
|
"""
|
||||||
|
Test Hyperopt.has_space() method
|
||||||
|
"""
|
||||||
|
_HYPEROPT.config.update({'spaces': ['buy', 'roi']})
|
||||||
|
assert _HYPEROPT.has_space('roi')
|
||||||
|
assert _HYPEROPT.has_space('buy')
|
||||||
|
assert not _HYPEROPT.has_space('stoploss')
|
||||||
|
|
||||||
|
_HYPEROPT.config.update({'spaces': ['all']})
|
||||||
|
assert _HYPEROPT.has_space('buy')
|
||||||
|
|
||||||
|
|
||||||
|
def test_populate_indicators() -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.populate_indicators()
|
||||||
|
"""
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
|
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
||||||
|
|
||||||
|
# Check if some indicators are generated. We will not test all of them
|
||||||
|
assert 'adx' in dataframe
|
||||||
|
assert 'ao' in dataframe
|
||||||
|
assert 'cci' in dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_strategy_generator() -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.buy_strategy_generator()
|
||||||
|
"""
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
||||||
|
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
|
||||||
|
|
||||||
|
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
||||||
|
{
|
||||||
|
'uptrend_long_ema': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'macd_below_zero': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'uptrend_short_ema': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
|
'mfi': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'fastd': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'adx': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'rsi': {
|
||||||
|
'enabled': True,
|
||||||
|
'value': 20
|
||||||
|
},
|
||||||
|
'over_sar': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
'green_candle': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
'uptrend_sma': {
|
||||||
|
'enabled': True,
|
||||||
|
},
|
||||||
|
|
||||||
|
'trigger': {
|
||||||
|
'type': 'lower_bb'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = populate_buy_trend(dataframe)
|
||||||
|
# Check if some indicators are generated. We will not test all of them
|
||||||
|
assert 'buy' in result
|
||||||
|
assert 1 in result['buy']
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Hyperopt.generate_optimizer() function
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.update({'config': 'config.json.example'})
|
||||||
|
conf.update({'timerange': None})
|
||||||
|
conf.update({'spaces': 'all'})
|
||||||
|
|
||||||
|
trades = [
|
||||||
|
('BTC_POWR', 0.023117, 0.000233, 100)
|
||||||
|
]
|
||||||
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
|
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
|
||||||
|
MagicMock(return_value=backtest_result)
|
||||||
|
)
|
||||||
|
|
||||||
|
optimizer_param = {
|
||||||
|
'adx': {'enabled': False},
|
||||||
|
'fastd': {'enabled': True, 'value': 35.0},
|
||||||
|
'green_candle': {'enabled': True},
|
||||||
|
'macd_below_zero': {'enabled': True},
|
||||||
|
'mfi': {'enabled': False},
|
||||||
|
'over_sar': {'enabled': False},
|
||||||
|
'roi_p1': 0.01,
|
||||||
|
'roi_p2': 0.01,
|
||||||
|
'roi_p3': 0.1,
|
||||||
|
'roi_t1': 60.0,
|
||||||
|
'roi_t2': 30.0,
|
||||||
|
'roi_t3': 20.0,
|
||||||
|
'rsi': {'enabled': False},
|
||||||
|
'stoploss': -0.4,
|
||||||
|
'trigger': {'type': 'macd_cross_signal'},
|
||||||
|
'uptrend_long_ema': {'enabled': False},
|
||||||
|
'uptrend_short_ema': {'enabled': True},
|
||||||
|
'uptrend_sma': {'enabled': True}
|
||||||
|
}
|
||||||
|
|
||||||
|
response_expected = {
|
||||||
|
'loss': 1.9840569076926293,
|
||||||
|
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||||
|
'(0.0231Σ%). Avg duration 100.0 mins.',
|
||||||
|
'status': 'ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
hyperopt = Hyperopt(conf)
|
||||||
|
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
|
||||||
|
assert generate_optimizer_value == response_expected
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
from user_data.hyperopt_conf import hyperopt_optimize_conf
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_optimize_conf():
|
def test_hyperopt_optimize_conf():
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import logging
|
import uuid
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from freqtrade import exchange, optimize
|
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade import optimize
|
||||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\
|
from freqtrade.misc import file_dump_json
|
||||||
download_backtesting_testdata, load_tickerdata_file
|
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
||||||
|
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
# Change this if modifying BTC_UNITEST testdatafile
|
# Change this if modifying BTC_UNITEST testdatafile
|
||||||
_btc_unittest_length = 13681
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
|
|
||||||
|
|
||||||
def _backup_file(file: str, copy_file: bool = False) -> None:
|
def _backup_file(file: str, copy_file: bool = False) -> None:
|
||||||
@ -43,65 +46,68 @@ def _clean_test_file(file: str) -> None:
|
|||||||
os.rename(file_swp, file)
|
os.rename(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test load_data() with 30 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, pairs=['BTC_ETH'])
|
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert ('freqtrade.optimize',
|
assert not log_has('Download the pair: "BTC_ETH", Interval: 30 min', caplog.record_tuples)
|
||||||
logging.INFO,
|
|
||||||
'Download the pair: "BTC_ETH", Interval: 5 min'
|
|
||||||
) not in caplog.record_tuples
|
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test load_data() with 5 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
||||||
|
_backup_file(file, copy_file=True)
|
||||||
|
optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert not log_has('Download the pair: "BTC_ETH", Interval: 5 min', caplog.record_tuples)
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test load_data() with 1 min ticker
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
|
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert ('freqtrade.optimize',
|
assert not log_has('Download the pair: "BTC_ETH", Interval: 1 min', caplog.record_tuples)
|
||||||
logging.INFO,
|
|
||||||
'Download the pair: "BTC_ETH", Interval: 1 min'
|
|
||||||
) not in caplog.record_tuples
|
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
|
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test load_data() with 1 min ticker
|
||||||
|
"""
|
||||||
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
_backup_file(file)
|
_backup_file(file)
|
||||||
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
|
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
|
||||||
assert os.path.isfile(file) is True
|
assert os.path.isfile(file) is True
|
||||||
assert ('freqtrade.optimize',
|
assert log_has('Download the pair: "BTC_MEME", Interval: 1 min', caplog.record_tuples)
|
||||||
logging.INFO,
|
|
||||||
'Download the pair: "BTC_MEME", Interval: 1 min'
|
|
||||||
) in caplog.record_tuples
|
|
||||||
_clean_test_file(file)
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
def test_testdata_path():
|
def test_testdata_path() -> None:
|
||||||
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs(default_conf, ticker_history, mocker):
|
def test_download_pairs(ticker_history, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
||||||
@ -113,46 +119,50 @@ def test_download_pairs(default_conf, ticker_history, mocker):
|
|||||||
_backup_file(file2_1)
|
_backup_file(file2_1)
|
||||||
_backup_file(file2_5)
|
_backup_file(file2_5)
|
||||||
|
|
||||||
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI']) is True
|
assert os.path.isfile(file1_1) is False
|
||||||
|
assert os.path.isfile(file2_1) is False
|
||||||
|
|
||||||
|
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=1) is True
|
||||||
|
|
||||||
assert os.path.isfile(file1_1) is True
|
assert os.path.isfile(file1_1) is True
|
||||||
assert os.path.isfile(file1_5) is True
|
|
||||||
assert os.path.isfile(file2_1) is True
|
assert os.path.isfile(file2_1) is True
|
||||||
assert os.path.isfile(file2_5) is True
|
|
||||||
|
|
||||||
# clean files freshly downloaded
|
# clean files freshly downloaded
|
||||||
_clean_test_file(file1_1)
|
_clean_test_file(file1_1)
|
||||||
_clean_test_file(file1_5)
|
|
||||||
_clean_test_file(file2_1)
|
_clean_test_file(file2_1)
|
||||||
|
|
||||||
|
assert os.path.isfile(file1_5) is False
|
||||||
|
assert os.path.isfile(file2_5) is False
|
||||||
|
|
||||||
|
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=5) is True
|
||||||
|
|
||||||
|
assert os.path.isfile(file1_5) is True
|
||||||
|
assert os.path.isfile(file2_5) is True
|
||||||
|
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_5)
|
||||||
_clean_test_file(file2_5)
|
_clean_test_file(file2_5)
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
|
def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=BaseException('File Error'))
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
||||||
_backup_file(file1_1)
|
_backup_file(file1_1)
|
||||||
_backup_file(file1_5)
|
_backup_file(file1_5)
|
||||||
|
|
||||||
download_pairs(None, pairs=['BTC-MEME'])
|
download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
|
||||||
# clean files freshly downloaded
|
# clean files freshly downloaded
|
||||||
_clean_test_file(file1_1)
|
_clean_test_file(file1_1)
|
||||||
_clean_test_file(file1_5)
|
_clean_test_file(file1_5)
|
||||||
assert ('freqtrade.optimize.__init__',
|
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
|
||||||
logging.INFO,
|
|
||||||
'Failed to download the pair: "BTC-MEME", Interval: 1 min'
|
|
||||||
) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
def test_download_backtesting_testdata(ticker_history, mocker) -> None:
|
||||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
# Download a 1 min ticker file
|
||||||
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
||||||
@ -170,7 +180,105 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
|||||||
_clean_test_file(file2)
|
_clean_test_file(file2)
|
||||||
|
|
||||||
|
|
||||||
def test_load_tickerdata_file():
|
def test_download_backtesting_testdata2(mocker) -> None:
|
||||||
|
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
||||||
|
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
||||||
|
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
|
||||||
|
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
||||||
|
assert json_dump_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_tickerdata_file() -> None:
|
||||||
|
# 7 does not exist in either format.
|
||||||
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
|
||||||
|
# 1 exists only as a .json
|
||||||
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
assert _btc_unittest_length == len(tickerdata)
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
|
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
|
||||||
|
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 8)
|
||||||
|
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
'',
|
||||||
|
pairs=[],
|
||||||
|
refresh_pairs=True,
|
||||||
|
ticker_interval=int(default_conf['ticker_interval'])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trim_tickerlist() -> None:
|
||||||
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
|
ticker_list = json.load(data_file)
|
||||||
|
ticker_list_len = len(ticker_list)
|
||||||
|
|
||||||
|
# Test the pattern ^(-\d+)$
|
||||||
|
# This pattern remove X element from the beginning
|
||||||
|
timerange = ((None, 'line'), None, 5)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_list_len == ticker_len + 5
|
||||||
|
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||||
|
assert ticker_list[-1] is ticker[-1] # The last element must be the same
|
||||||
|
|
||||||
|
# Test the pattern ^(\d+)-$
|
||||||
|
# This pattern keep X element from the end
|
||||||
|
timerange = (('line', None), 5, None)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == 5
|
||||||
|
assert ticker_list[0] is ticker[0] # The first element must be the same
|
||||||
|
assert ticker_list[-1] is not ticker[-1] # The last element should be different
|
||||||
|
|
||||||
|
# Test the pattern ^(\d+)-(\d+)$
|
||||||
|
# This pattern extract a window
|
||||||
|
timerange = (('index', 'index'), 5, 10)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_len == 5
|
||||||
|
assert ticker_list[0] is not ticker[0] # The first element should be different
|
||||||
|
assert ticker_list[5] is ticker[0] # The list starts at the index 5
|
||||||
|
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
|
||||||
|
|
||||||
|
# Test a wrong pattern
|
||||||
|
# This pattern must return the list unchanged
|
||||||
|
timerange = ((None, None), None, 5)
|
||||||
|
ticker = trim_tickerlist(ticker_list, timerange)
|
||||||
|
ticker_len = len(ticker)
|
||||||
|
|
||||||
|
assert ticker_list_len == ticker_len
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_dump_json() -> None:
|
||||||
|
"""
|
||||||
|
Test file_dump_json()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
|
||||||
|
data = {'bar': 'foo'}
|
||||||
|
|
||||||
|
# check the file we will create does not exist
|
||||||
|
assert os.path.isfile(file) is False
|
||||||
|
|
||||||
|
# Create the Json file
|
||||||
|
file_dump_json(file, data)
|
||||||
|
|
||||||
|
# Check the file was create
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
|
||||||
|
# Open the Json file created and test the data is in it
|
||||||
|
with open(file) as data_file:
|
||||||
|
json_from_file = json.load(data_file)
|
||||||
|
|
||||||
|
assert 'bar' in json_from_file
|
||||||
|
assert json_from_file['bar'] == 'foo'
|
||||||
|
|
||||||
|
# Remove the file
|
||||||
|
_clean_test_file(file)
|
||||||
|
@ -1,57 +1,544 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||||
from copy import deepcopy
|
|
||||||
|
"""
|
||||||
|
Unit test file for rpc/rpc.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.rpc import init, cleanup, send_msg
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.rpc import RPC
|
||||||
|
from freqtrade.state import State
|
||||||
|
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_enabled(default_conf, mocker):
|
# Functions for recurrent object patching
|
||||||
module_list = []
|
def prec_satoshi(a, b) -> float:
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
"""
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
:return: True if A and B differs less than one satoshi.
|
||||||
|
"""
|
||||||
init(default_conf)
|
return abs(a - b) < 0.00000001
|
||||||
|
|
||||||
assert telegram_mock.call_count == 1
|
|
||||||
assert 'telegram' in module_list
|
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_disabled(default_conf, mocker):
|
# Unit tests
|
||||||
module_list = []
|
def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
"""
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
Test rpc_trade_status() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
conf['telegram']['enabled'] = False
|
rpc = RPC(freqtradebot)
|
||||||
init(conf)
|
|
||||||
|
|
||||||
assert telegram_mock.call_count == 0
|
freqtradebot.state = State.STOPPED
|
||||||
assert 'telegram' not in module_list
|
(error, result) = rpc.rpc_trade_status()
|
||||||
|
assert error
|
||||||
|
assert 'trader is not running' in result
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
(error, result) = rpc.rpc_trade_status()
|
||||||
|
assert error
|
||||||
|
assert 'no active trade' in result
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, result) = rpc.rpc_trade_status()
|
||||||
|
assert not error
|
||||||
|
trade = result[0]
|
||||||
|
|
||||||
|
result_message = [
|
||||||
|
'*Trade ID:* `1`\n'
|
||||||
|
'*Current Pair:* '
|
||||||
|
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
||||||
|
'*Open Since:* `just now`\n'
|
||||||
|
'*Amount:* `90.99181074`\n'
|
||||||
|
'*Open Rate:* `0.00001099`\n'
|
||||||
|
'*Close Rate:* `None`\n'
|
||||||
|
'*Current Rate:* `0.00001098`\n'
|
||||||
|
'*Close Profit:* `None`\n'
|
||||||
|
'*Current Profit:* `-0.59%`\n'
|
||||||
|
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
|
||||||
|
]
|
||||||
|
assert result == result_message
|
||||||
|
assert trade.find('[BTC_ETH]') >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_enabled(mocker):
|
def test_rpc_status_table(default_conf, ticker, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
"""
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
Test rpc_status_table() method
|
||||||
cleanup()
|
"""
|
||||||
assert telegram_mock.call_count == 1
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.STOPPED
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `trader is not running`' in result
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `no active order`' in result
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, result) = rpc.rpc_status_table()
|
||||||
|
assert 'just now' in result['Since'].all()
|
||||||
|
assert 'BTC_ETH' in result['Pair'].all()
|
||||||
|
assert '-0.59%' in result['Profit'].all()
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_disabled(mocker):
|
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
-> None:
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
"""
|
||||||
cleanup()
|
Test rpc_daily_profit() method
|
||||||
assert telegram_mock.call_count == 0
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
stake_currency = default_conf['stake_currency']
|
||||||
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Simulate buy & sell
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
# Try valid data
|
||||||
|
update.message.text = '/daily 2'
|
||||||
|
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
|
||||||
|
assert not error
|
||||||
|
assert len(days) == 7
|
||||||
|
for day in days:
|
||||||
|
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
|
||||||
|
assert (day[1] == '0.00000000 BTC' or
|
||||||
|
day[1] == '0.00006217 BTC')
|
||||||
|
|
||||||
|
assert (day[2] == '0.000 USD' or
|
||||||
|
day[2] == '0.933 USD')
|
||||||
|
# ensure first day is current date
|
||||||
|
assert str(days[0][0]) == str(datetime.utcnow().date())
|
||||||
|
|
||||||
|
# Try invalid data
|
||||||
|
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||||
|
assert error
|
||||||
|
assert days.find('must be an integer greater than 0') >= 0
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_telegram_enabled(mocker):
|
def test_rpc_trade_statistics(
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
"""
|
||||||
send_msg('test')
|
Test rpc_trade_statistics() method
|
||||||
assert telegram_mock.call_count == 1
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
stake_currency = default_conf['stake_currency']
|
||||||
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
|
assert error
|
||||||
|
assert stats.find('no closed trade') >= 0
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
# Update the ticker with a market going up
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
(error, 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_percent'], 6.2)
|
||||||
|
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
|
||||||
|
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
|
||||||
|
assert prec_satoshi(stats['profit_all_percent'], 6.2)
|
||||||
|
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
|
||||||
|
assert stats['trade_count'] == 1
|
||||||
|
assert stats['first_trade_date'] == 'just now'
|
||||||
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
|
assert stats['best_pair'] == 'BTC_ETH'
|
||||||
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_telegram_disabled(mocker):
|
# Test that rpc_trade_statistics can handle trades that lacks
|
||||||
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
# trade.open_rate (it is set to None)
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
|
||||||
send_msg('test')
|
limit_sell_order):
|
||||||
assert telegram_mock.call_count == 0
|
"""
|
||||||
|
Test rpc_trade_statistics() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
stake_currency = default_conf['stake_currency']
|
||||||
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
# Update the ticker with a market going up
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up
|
||||||
|
)
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
for trade in Trade.query.order_by(Trade.id).all():
|
||||||
|
trade.open_rate = None
|
||||||
|
|
||||||
|
(error, 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_percent'], 0)
|
||||||
|
assert prec_satoshi(stats['profit_closed_fiat'], 0)
|
||||||
|
assert prec_satoshi(stats['profit_all_coin'], 0)
|
||||||
|
assert prec_satoshi(stats['profit_all_percent'], 0)
|
||||||
|
assert prec_satoshi(stats['profit_all_fiat'], 0)
|
||||||
|
assert stats['trade_count'] == 1
|
||||||
|
assert stats['first_trade_date'] == 'just now'
|
||||||
|
assert stats['latest_trade_date'] == 'just now'
|
||||||
|
assert stats['avg_duration'] == '0:00:00'
|
||||||
|
assert stats['best_pair'] == 'BTC_ETH'
|
||||||
|
assert prec_satoshi(stats['best_rate'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_balance_handle(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test rpc_balance() method
|
||||||
|
"""
|
||||||
|
mock_balance = [
|
||||||
|
{
|
||||||
|
'Currency': 'BTC',
|
||||||
|
'Balance': 10.0,
|
||||||
|
'Available': 12.0,
|
||||||
|
'Pending': 0.0,
|
||||||
|
'CryptoAddress': 'XXXX',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Currency': 'ETH',
|
||||||
|
'Balance': 0.0,
|
||||||
|
'Available': 0.0,
|
||||||
|
'Pending': 0.0,
|
||||||
|
'CryptoAddress': 'XXXX',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=mock_balance)
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
|
||||||
|
assert not error
|
||||||
|
(trade, x, y, z) = res
|
||||||
|
assert prec_satoshi(x, 10)
|
||||||
|
assert prec_satoshi(z, 150000)
|
||||||
|
assert 'USD' in y
|
||||||
|
assert len(trade) == 1
|
||||||
|
assert 'BTC' in trade[0]['currency']
|
||||||
|
assert prec_satoshi(trade[0]['available'], 12)
|
||||||
|
assert prec_satoshi(trade[0]['balance'], 10)
|
||||||
|
assert prec_satoshi(trade[0]['pending'], 0)
|
||||||
|
assert prec_satoshi(trade[0]['est_btc'], 10)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_start() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock()
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
freqtradebot.state = State.STOPPED
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_start()
|
||||||
|
assert not error
|
||||||
|
assert '`Starting trader ...`' in result
|
||||||
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_start()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `already running`' in result
|
||||||
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_stop(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_stop() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock()
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_stop()
|
||||||
|
assert not error
|
||||||
|
assert '`Stopping trader ...`' in result
|
||||||
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
|
(error, result) = rpc.rpc_stop()
|
||||||
|
assert error
|
||||||
|
assert '*Status:* `already stopped`' in result
|
||||||
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_forcesell() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker,
|
||||||
|
cancel_order=cancel_order_mock,
|
||||||
|
get_order=MagicMock(
|
||||||
|
return_value={
|
||||||
|
'closed': True,
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
freqtradebot.state = State.STOPPED
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == 'Invalid argument.'
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('1')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
|
||||||
|
freqtradebot.state = State.STOPPED
|
||||||
|
(error, res) = rpc.rpc_forcesell(None)
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
(error, res) = rpc.rpc_forcesell('all')
|
||||||
|
assert error
|
||||||
|
assert res == '`trader is not running`'
|
||||||
|
|
||||||
|
freqtradebot.state = State.RUNNING
|
||||||
|
assert cancel_order_mock.call_count == 0
|
||||||
|
# make an limit-buy open trade
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'closed': None,
|
||||||
|
'type': 'LIMIT_BUY'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# check that the trade is called, which is done
|
||||||
|
# by ensuring exchange.cancel_order is called
|
||||||
|
(error, res) = rpc.rpc_forcesell('1')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
# make an limit-sell open trade
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.freqtradebot.exchange.get_order',
|
||||||
|
return_value={
|
||||||
|
'closed': None,
|
||||||
|
'type': 'LIMIT_SELL'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(error, res) = rpc.rpc_forcesell('2')
|
||||||
|
assert not error
|
||||||
|
assert res == ''
|
||||||
|
# status quo, no exchange calls
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_performance_handle(default_conf, ticker, limit_buy_order,
|
||||||
|
limit_sell_order, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_performance() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
(error, res) = rpc.rpc_performance()
|
||||||
|
assert not error
|
||||||
|
assert len(res) == 1
|
||||||
|
assert res[0]['pair'] == 'BTC_ETH'
|
||||||
|
assert res[0]['count'] == 1
|
||||||
|
assert prec_satoshi(res[0]['profit'], 6.2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_count(mocker, default_conf, ticker) -> None:
|
||||||
|
"""
|
||||||
|
Test rpc_count() method
|
||||||
|
"""
|
||||||
|
patch_get_signal(mocker, (True, False))
|
||||||
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_balances=MagicMock(return_value=ticker),
|
||||||
|
get_ticker=ticker
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
|
(error, trades) = rpc.rpc_count()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert not error
|
||||||
|
assert nb_trades == 0
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtradebot.create_trade()
|
||||||
|
(error, trades) = rpc.rpc_count()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert not error
|
||||||
|
assert nb_trades == 1
|
||||||
|
139
freqtrade/tests/rpc/test_rpc_manager.py
Normal file
139
freqtrade/tests/rpc/test_rpc_manager.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for rpc/rpc_manager.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade.rpc.rpc_manager import RPCManager
|
||||||
|
from freqtrade.rpc.telegram import Telegram
|
||||||
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
|
def test_rpc_manager_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Arguments object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(RPCManager, '_init')
|
||||||
|
assert hasattr(RPCManager, 'send_msg')
|
||||||
|
assert hasattr(RPCManager, 'cleanup')
|
||||||
|
|
||||||
|
|
||||||
|
def test__init__(mocker, default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test __init__() method
|
||||||
|
"""
|
||||||
|
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
assert rpc_manager.freqtrade == freqtradebot
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Test _init() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert rpc_manager.registered_modules == []
|
||||||
|
assert rpc_manager.telegram is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test _init() method with Telegram enabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
|
len_modules = len(rpc_manager.registered_modules)
|
||||||
|
assert len_modules == 1
|
||||||
|
assert 'telegram' in rpc_manager.registered_modules
|
||||||
|
assert isinstance(rpc_manager.telegram, Telegram)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test cleanup() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.cleanup()
|
||||||
|
|
||||||
|
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test cleanup() method with Telegram enabled
|
||||||
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
|
||||||
|
# Check we have Telegram as a registered modules
|
||||||
|
assert 'telegram' in rpc_manager.registered_modules
|
||||||
|
|
||||||
|
rpc_manager.cleanup()
|
||||||
|
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
|
||||||
|
assert 'telegram' not in rpc_manager.registered_modules
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test send_msg() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
|
assert log_has('test', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test send_msg() method with Telegram disabled
|
||||||
|
"""
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
rpc_manager.send_msg('test')
|
||||||
|
|
||||||
|
assert log_has('test', caplog.record_tuples)
|
||||||
|
assert telegram_mock.call_count == 1
|
File diff suppressed because it is too large
Load Diff
34
freqtrade/tests/strategy/test_default_strategy.py
Normal file
34
freqtrade/tests/strategy/test_default_strategy.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def result():
|
||||||
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
|
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_strategy_structure():
|
||||||
|
assert hasattr(DefaultStrategy, 'minimal_roi')
|
||||||
|
assert hasattr(DefaultStrategy, 'stoploss')
|
||||||
|
assert hasattr(DefaultStrategy, 'ticker_interval')
|
||||||
|
assert hasattr(DefaultStrategy, 'populate_indicators')
|
||||||
|
assert hasattr(DefaultStrategy, 'populate_buy_trend')
|
||||||
|
assert hasattr(DefaultStrategy, 'populate_sell_trend')
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_strategy(result):
|
||||||
|
strategy = DefaultStrategy()
|
||||||
|
|
||||||
|
assert type(strategy.minimal_roi) is dict
|
||||||
|
assert type(strategy.stoploss) is float
|
||||||
|
assert type(strategy.ticker_interval) is int
|
||||||
|
indicators = strategy.populate_indicators(result)
|
||||||
|
assert type(indicators) is DataFrame
|
||||||
|
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
||||||
|
assert type(strategy.populate_sell_trend(indicators)) is DataFrame
|
118
freqtrade/tests/strategy/test_strategy.py
Normal file
118
freqtrade/tests/strategy/test_strategy.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_strategy():
|
||||||
|
default_location = os.path.join(os.path.dirname(
|
||||||
|
os.path.realpath(__file__)), '..', '..', 'strategy'
|
||||||
|
)
|
||||||
|
assert isinstance(
|
||||||
|
StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy
|
||||||
|
)
|
||||||
|
assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_strategy(result):
|
||||||
|
resolver = StrategyResolver()
|
||||||
|
resolver._load_strategy('TestStrategy')
|
||||||
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_strategy_custom_directory(result):
|
||||||
|
resolver = StrategyResolver()
|
||||||
|
extra_dir = os.path.join('some', 'path')
|
||||||
|
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 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_not_found_strategy():
|
||||||
|
strategy = StrategyResolver()
|
||||||
|
with pytest.raises(ImportError,
|
||||||
|
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
|
||||||
|
r' This class does not exist or contains Python code errors'):
|
||||||
|
strategy._load_strategy('NotFoundStrategy')
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy(result):
|
||||||
|
resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||||
|
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'stoploss')
|
||||||
|
assert resolver.strategy.stoploss == -0.10
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'populate_indicators')
|
||||||
|
assert 'adx' in resolver.strategy.populate_indicators(result)
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'populate_buy_trend')
|
||||||
|
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result))
|
||||||
|
assert 'buy' in dataframe.columns
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'populate_sell_trend')
|
||||||
|
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
|
||||||
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_minimal_roi(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'minimal_roi': {
|
||||||
|
"0": 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'minimal_roi')
|
||||||
|
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||||
|
assert ('freqtrade.strategy.resolver',
|
||||||
|
logging.INFO,
|
||||||
|
'Override strategy \'minimal_roi\' with value in config file.'
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_stoploss(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'stoploss': -0.5
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'stoploss')
|
||||||
|
assert resolver.strategy.stoploss == -0.5
|
||||||
|
assert ('freqtrade.strategy.resolver',
|
||||||
|
logging.INFO,
|
||||||
|
'Override strategy \'stoploss\' with value in config file: -0.5.'
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_ticker_interval(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'ticker_interval': 60
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert hasattr(resolver.strategy, 'ticker_interval')
|
||||||
|
assert resolver.strategy.ticker_interval == 60
|
||||||
|
assert ('freqtrade.strategy.resolver',
|
||||||
|
logging.INFO,
|
||||||
|
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
||||||
|
) in caplog.record_tuples
|
@ -1,4 +1,6 @@
|
|||||||
from freqtrade.main import refresh_whitelist, gen_pair_whitelist
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
|
import freqtrade.tests.conftest as tt # test tools
|
||||||
|
|
||||||
# whitelist, blacklist, filtering, all of that will
|
# whitelist, blacklist, filtering, all of that will
|
||||||
# eventually become some rules to run on a generic ACL engine
|
# eventually become some rules to run on a generic ACL engine
|
||||||
@ -6,21 +8,22 @@ from freqtrade.main import refresh_whitelist, gen_pair_whitelist
|
|||||||
|
|
||||||
|
|
||||||
def whitelist_conf():
|
def whitelist_conf():
|
||||||
return {
|
config = tt.default_conf()
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'exchange': {
|
config['stake_currency'] = 'BTC'
|
||||||
'pair_whitelist': [
|
config['exchange']['pair_whitelist'] = [
|
||||||
'BTC_ETH',
|
'BTC_ETH',
|
||||||
'BTC_TKN',
|
'BTC_TKN',
|
||||||
'BTC_TRST',
|
'BTC_TRST',
|
||||||
'BTC_SWT',
|
'BTC_SWT',
|
||||||
'BTC_BCC'
|
'BTC_BCC'
|
||||||
],
|
]
|
||||||
'pair_blacklist': [
|
|
||||||
'BTC_BLK'
|
config['exchange']['pair_blacklist'] = [
|
||||||
],
|
'BTC_BLK'
|
||||||
},
|
]
|
||||||
}
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_market_summaries():
|
def get_market_summaries():
|
||||||
@ -73,16 +76,9 @@ def get_market_summaries():
|
|||||||
|
|
||||||
|
|
||||||
def get_health():
|
def get_health():
|
||||||
return [{'Currency': 'ETH',
|
return [{'Currency': 'ETH', 'IsActive': True},
|
||||||
'IsActive': True
|
{'Currency': 'TKN', 'IsActive': True},
|
||||||
},
|
{'Currency': 'BLK', 'IsActive': True}]
|
||||||
{'Currency': 'TKN',
|
|
||||||
'IsActive': True
|
|
||||||
},
|
|
||||||
{'Currency': 'BLK',
|
|
||||||
'IsActive': True
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_health_empty():
|
def get_health_empty():
|
||||||
@ -91,11 +87,13 @@ def get_health_empty():
|
|||||||
|
|
||||||
def test_refresh_market_pair_not_in_whitelist(mocker):
|
def test_refresh_market_pair_not_in_whitelist(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
get_wallet_health=get_health)
|
|
||||||
refreshedwhitelist = refresh_whitelist(
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
||||||
conf['exchange']['pair_whitelist'] + ['BTC_XXX'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
|
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
|
||||||
|
)
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
@ -104,10 +102,11 @@ def test_refresh_market_pair_not_in_whitelist(mocker):
|
|||||||
|
|
||||||
def test_refresh_whitelist(mocker):
|
def test_refresh_whitelist(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
get_wallet_health=get_health)
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
|
||||||
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['BTC_ETH', 'BTC_TKN']
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
# Ensure all except those in whitelist are removed
|
# Ensure all except those in whitelist are removed
|
||||||
@ -116,26 +115,32 @@ def test_refresh_whitelist(mocker):
|
|||||||
|
|
||||||
def test_refresh_whitelist_dynamic(mocker):
|
def test_refresh_whitelist_dynamic(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
get_wallet_health=get_health)
|
'freqtrade.freqtradebot.exchange',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
get_wallet_health=get_health,
|
||||||
get_market_summaries=get_market_summaries)
|
get_market_summaries=get_market_summaries
|
||||||
|
)
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = ['BTC_TKN', 'BTC_ETH']
|
whitelist = ['BTC_TKN', 'BTC_ETH']
|
||||||
refreshedwhitelist = refresh_whitelist(
|
|
||||||
gen_pair_whitelist(conf['stake_currency']))
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
|
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
||||||
|
)
|
||||||
|
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic_empty(mocker):
|
def test_refresh_whitelist_dynamic_empty(mocker):
|
||||||
conf = whitelist_conf()
|
conf = whitelist_conf()
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
|
||||||
get_wallet_health=get_health_empty)
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = []
|
whitelist = []
|
||||||
conf['exchange']['pair_whitelist'] = []
|
conf['exchange']['pair_whitelist'] = []
|
||||||
refresh_whitelist(whitelist)
|
freqtradebot._refresh_whitelist(whitelist)
|
||||||
pairslist = conf['exchange']['pair_whitelist']
|
pairslist = conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
assert set(whitelist) == set(pairslist)
|
assert set(whitelist) == set(pairslist)
|
||||||
|
@ -1,74 +1,194 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0621
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import json
|
|
||||||
|
"""
|
||||||
|
Unit test file for analyse.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe,
|
from freqtrade.analyze import Analyze, SignalType
|
||||||
populate_buy_trend, populate_indicators,
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
populate_sell_trend)
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
# Avoid to reinit the same object again and again
|
||||||
|
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
def test_signaltype_object() -> None:
|
||||||
def result():
|
"""
|
||||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
Test the SignalType object has the mandatory Constants
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(SignalType, 'BUY')
|
||||||
|
assert hasattr(SignalType, 'SELL')
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Analyze object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(Analyze, 'parse_ticker_dataframe')
|
||||||
|
assert hasattr(Analyze, 'populate_indicators')
|
||||||
|
assert hasattr(Analyze, 'populate_buy_trend')
|
||||||
|
assert hasattr(Analyze, 'populate_sell_trend')
|
||||||
|
assert hasattr(Analyze, 'analyze_ticker')
|
||||||
|
assert hasattr(Analyze, 'get_signal')
|
||||||
|
assert hasattr(Analyze, 'should_sell')
|
||||||
|
assert hasattr(Analyze, 'min_roi_reached')
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_correct_length(result):
|
||||||
|
dataframe = Analyze.parse_ticker_dataframe(result)
|
||||||
|
assert len(result.index) == len(dataframe.index)
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
def test_dataframe_correct_columns(result):
|
||||||
assert result.columns.tolist() == \
|
assert result.columns.tolist() == \
|
||||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
['date', 'close', 'high', 'low', 'open', 'volume']
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
|
||||||
assert len(result.index) == 14395
|
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||||
|
dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_populates_sell_trend(result):
|
def test_populates_sell_trend(result):
|
||||||
dataframe = populate_sell_trend(populate_indicators(result))
|
# Load the default strategy for the unit test, because this logic is done in main.py
|
||||||
|
dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
|
||||||
assert 'sell' in dataframe.columns
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.analyze.analyze_ticker',
|
|
||||||
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
|
||||||
)
|
|
||||||
assert get_signal('BTC-ETH', SignalType.BUY)
|
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.analyze_ticker',
|
'freqtrade.analyze.Analyze',
|
||||||
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker):
|
def test_returns_latest_sell_signal(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch(
|
mocker.patch.multiple(
|
||||||
'freqtrade.analyze.analyze_ticker',
|
'freqtrade.analyze.Analyze',
|
||||||
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert get_signal('BTC-ETH', SignalType.SELL)
|
|
||||||
|
|
||||||
mocker.patch(
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
|
||||||
'freqtrade.analyze.analyze_ticker',
|
|
||||||
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
||||||
|
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
side_effect=ValueError('xyz')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
|
||||||
|
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame([])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
||||||
|
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=1)
|
||||||
|
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
||||||
|
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
|
||||||
|
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
return_value=DataFrame(ticks)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
|
||||||
|
assert log_has(
|
||||||
|
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
||||||
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert not get_signal('BTC-ETH', SignalType.SELL)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker):
|
def test_get_signal_handles_exceptions(mocker):
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
mocker.patch.multiple(
|
||||||
side_effect=Exception('invalid ticker history '))
|
'freqtrade.analyze.Analyze',
|
||||||
|
analyze_ticker=MagicMock(
|
||||||
|
side_effect=Exception('invalid ticker history ')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
|
||||||
|
columns = ['date', 'close', 'high', 'low', 'open', 'volume']
|
||||||
|
|
||||||
|
# Test file with BV data
|
||||||
|
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
||||||
|
assert dataframe.columns.tolist() == columns
|
||||||
|
|
||||||
|
# Test file without BV data
|
||||||
|
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
|
||||||
|
assert dataframe.columns.tolist() == columns
|
||||||
|
|
||||||
|
|
||||||
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
"""
|
||||||
|
Test Analyze.tickerdata_to_dataframe() method
|
||||||
|
"""
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
|
||||||
|
timerange = ((None, 'line'), None, -100)
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
data = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert len(data['BTC_UNITEST']) == 100
|
||||||
|
154
freqtrade/tests/test_arguments.py
Normal file
154
freqtrade/tests/test_arguments.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for arguments.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_arguments_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Arguments object has the mandatory methods
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(Arguments, 'get_parsed_arg')
|
||||||
|
assert hasattr(Arguments, 'parse_args')
|
||||||
|
assert hasattr(Arguments, 'parse_timerange')
|
||||||
|
assert hasattr(Arguments, 'scripts_options')
|
||||||
|
|
||||||
|
|
||||||
|
# Parse common command-line-arguments. Used for all tools
|
||||||
|
def test_parse_args_none() -> None:
|
||||||
|
arguments = Arguments([], '')
|
||||||
|
assert isinstance(arguments, Arguments)
|
||||||
|
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||||
|
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_defaults() -> None:
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
assert args.config == 'config.json'
|
||||||
|
assert args.dynamic_whitelist is None
|
||||||
|
assert args.loglevel == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_config() -> None:
|
||||||
|
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_verbose() -> None:
|
||||||
|
args = Arguments(['-v'], '').get_parsed_arg()
|
||||||
|
assert args.loglevel == logging.DEBUG
|
||||||
|
|
||||||
|
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||||
|
assert args.loglevel == logging.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
def test_scripts_options() -> None:
|
||||||
|
arguments = Arguments(['-p', 'BTC_ETH'], '')
|
||||||
|
arguments.scripts_options()
|
||||||
|
args = arguments.get_parsed_arg()
|
||||||
|
assert args.pair == 'BTC_ETH'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_version() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'0'):
|
||||||
|
Arguments(['--version'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['-c'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_strategy() -> None:
|
||||||
|
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
|
||||||
|
assert args.strategy == 'SomeStrategy'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_strategy_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['--strategy'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_strategy_path() -> None:
|
||||||
|
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
|
||||||
|
assert args.strategy_path == '/some/path'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_strategy_path_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['--strategy-path'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist() -> None:
|
||||||
|
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
|
||||||
|
assert args.dynamic_whitelist == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_10() -> None:
|
||||||
|
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
|
||||||
|
assert args.dynamic_whitelist == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_timerange_incorrect() -> None:
|
||||||
|
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
|
||||||
|
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
|
||||||
|
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
|
||||||
|
Arguments.parse_timerange('-')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_invalid() -> None:
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_custom() -> None:
|
||||||
|
args = [
|
||||||
|
'-c', 'test_conf.json',
|
||||||
|
'backtesting',
|
||||||
|
'--live',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--refresh-pairs-cached']
|
||||||
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.live is True
|
||||||
|
assert call_args.loglevel == logging.INFO
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 1
|
||||||
|
assert call_args.refresh_pairs is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_hyperopt_custom() -> None:
|
||||||
|
args = [
|
||||||
|
'-c', 'test_conf.json',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '20',
|
||||||
|
'--spaces', 'buy'
|
||||||
|
]
|
||||||
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.epochs == 20
|
||||||
|
assert call_args.loglevel == logging.INFO
|
||||||
|
assert call_args.subparser == 'hyperopt'
|
||||||
|
assert call_args.spaces == ['buy']
|
||||||
|
assert call_args.func is not None
|
336
freqtrade/tests/test_configuration.py
Normal file
336
freqtrade/tests/test_configuration.py
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
# pragma pylint: disable=protected-access, invalid-name
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unit test file for configuration.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Constants object has the mandatory Constants
|
||||||
|
"""
|
||||||
|
assert hasattr(Configuration, 'load_config')
|
||||||
|
assert hasattr(Configuration, '_load_config_file')
|
||||||
|
assert hasattr(Configuration, '_validate_config')
|
||||||
|
assert hasattr(Configuration, '_load_common_config')
|
||||||
|
assert hasattr(Configuration, '_load_backtesting_config')
|
||||||
|
assert hasattr(Configuration, '_load_hyperopt_config')
|
||||||
|
assert hasattr(Configuration, 'get_config')
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_invalid_pair(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with an invalid PAIR format
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
|
configuration = Configuration([])
|
||||||
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_missing_attributes(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test the configuration validator with a missing attribute
|
||||||
|
"""
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.pop('exchange')
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
|
configuration = Configuration([])
|
||||||
|
configuration._validate_config(conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration._load_config_file() method
|
||||||
|
"""
|
||||||
|
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
configuration = Configuration([])
|
||||||
|
validated_conf = configuration._load_config_file('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
assert 'internals' in validated_conf
|
||||||
|
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_file_exception(mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration._load_config_file() method
|
||||||
|
"""
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.open',
|
||||||
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
|
)
|
||||||
|
configuration = Configuration([])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
configuration._load_config_file('somefile')
|
||||||
|
assert log_has(
|
||||||
|
'Config file "somefile" not found. Please create your config file',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.load_config() without any cli params
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert validated_conf.get('strategy') == 'DefaultStrategy'
|
||||||
|
assert validated_conf.get('strategy_path') is None
|
||||||
|
assert 'dynamic_whitelist' not in validated_conf
|
||||||
|
assert 'dry_run_db' not in validated_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_with_params(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.load_config() with cli params used
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--strategy-path', '/some/path',
|
||||||
|
'--dry-run-db',
|
||||||
|
]
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert validated_conf.get('dynamic_whitelist') == 10
|
||||||
|
assert validated_conf.get('strategy') == 'TestStrategy'
|
||||||
|
assert validated_conf.get('strategy_path') == '/some/path'
|
||||||
|
assert validated_conf.get('dry_run_db') is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.load_config() without any cli params
|
||||||
|
"""
|
||||||
|
custom_conf = deepcopy(default_conf)
|
||||||
|
custom_conf.update({
|
||||||
|
'strategy': 'CustomStrategy',
|
||||||
|
'strategy_path': '/tmp/strategies',
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(custom_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = Arguments([], '').get_parsed_arg()
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert validated_conf.get('strategy') == 'CustomStrategy'
|
||||||
|
assert validated_conf.get('strategy_path') == '/tmp/strategies'
|
||||||
|
|
||||||
|
|
||||||
|
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test Configuration.show_info()
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--dynamic-whitelist', '10',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'--dry-run-db'
|
||||||
|
]
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
configuration.get_config()
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --dynamic-whitelist detected. '
|
||||||
|
'Using dynamically generated whitelist. '
|
||||||
|
'(not applicable with Backtesting and Hyperopt)',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --dry-run-db detected ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert log_has(
|
||||||
|
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the Dry run condition
|
||||||
|
configuration.config.update({'dry_run': False})
|
||||||
|
configuration._load_common_config(configuration.config)
|
||||||
|
assert log_has(
|
||||||
|
'Dry run is disabled. (--dry_run_db ignored)',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'backtesting'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'live' not in config
|
||||||
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation' not in config
|
||||||
|
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs' not in config
|
||||||
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
assert 'export' not in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'DefaultStrategy',
|
||||||
|
'--datadir', '/foo/bar',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--live',
|
||||||
|
'--realistic-simulation',
|
||||||
|
'--refresh-pairs-cached',
|
||||||
|
'--timerange', ':100',
|
||||||
|
'--export', '/bar/foo'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
assert 'max_open_trades' in config
|
||||||
|
assert 'stake_currency' in config
|
||||||
|
assert 'stake_amount' in config
|
||||||
|
assert 'exchange' in config
|
||||||
|
assert 'pair_whitelist' in config['exchange']
|
||||||
|
assert 'datadir' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --datadir detected: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1 ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'live' in config
|
||||||
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'realistic_simulation'in config
|
||||||
|
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'refresh_pairs'in config
|
||||||
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
|
assert 'timerange' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --timerange detected: {} ...'.format(config['timerange']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'export' in config
|
||||||
|
assert log_has(
|
||||||
|
'Parameter --export detected: {} ...'.format(config['export']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
|
"""
|
||||||
|
Test setup_configuration() function
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '10',
|
||||||
|
'--use-mongodb',
|
||||||
|
'--spaces', 'all',
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(args, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
config = configuration.get_config()
|
||||||
|
|
||||||
|
assert 'epochs' in config
|
||||||
|
assert int(config['epochs']) == 10
|
||||||
|
assert log_has('Parameter --epochs detected ...', 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 config['spaces'] == ['all']
|
||||||
|
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
|
25
freqtrade/tests/test_constants.py
Normal file
25
freqtrade/tests/test_constants.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for constants.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from freqtrade import constants
|
||||||
|
|
||||||
|
|
||||||
|
def test_constant_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the Constants object has the mandatory Constants
|
||||||
|
"""
|
||||||
|
assert hasattr(constants, 'CONF_SCHEMA')
|
||||||
|
assert hasattr(constants, 'DYNAMIC_WHITELIST')
|
||||||
|
assert hasattr(constants, 'PROCESS_THROTTLE_SECS')
|
||||||
|
assert hasattr(constants, 'TICKER_INTERVAL')
|
||||||
|
assert hasattr(constants, 'HYPEROPT_EPOCH')
|
||||||
|
assert hasattr(constants, 'RETRY_TIMEOUT')
|
||||||
|
assert hasattr(constants, 'DEFAULT_STRATEGY')
|
||||||
|
|
||||||
|
|
||||||
|
def test_conf_schema() -> None:
|
||||||
|
"""
|
||||||
|
Test the CONF_SCHEMA is from the right type
|
||||||
|
"""
|
||||||
|
assert isinstance(constants.CONF_SCHEMA, dict)
|
@ -1,26 +1,33 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
import pandas
|
import pandas
|
||||||
|
|
||||||
import freqtrade.optimize
|
from freqtrade.analyze import Analyze
|
||||||
from freqtrade import analyze
|
from freqtrade.optimize import load_data
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
_pairs = ['BTC_ETH']
|
_pairs = ['BTC_ETH']
|
||||||
|
|
||||||
|
|
||||||
def load_dataframe_pair(pairs):
|
def load_dataframe_pair(pairs):
|
||||||
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs)
|
ld = load_data(None, ticker_interval=5, pairs=pairs)
|
||||||
assert isinstance(ld, dict)
|
assert isinstance(ld, dict)
|
||||||
assert isinstance(pairs[0], str)
|
assert isinstance(pairs[0], str)
|
||||||
dataframe = ld[pairs[0]]
|
dataframe = ld[pairs[0]]
|
||||||
|
|
||||||
|
analyze = Analyze({'strategy': 'DefaultStrategy'})
|
||||||
dataframe = analyze.analyze_ticker(dataframe)
|
dataframe = analyze.analyze_ticker(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_load():
|
def test_dataframe_load():
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_columns_exists():
|
def test_dataframe_columns_exists():
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
assert 'high' in dataframe.columns
|
assert 'high' in dataframe.columns
|
||||||
assert 'low' in dataframe.columns
|
assert 'low' in dataframe.columns
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors,
|
||||||
|
# pragma pylint: disable=protected-access, C0103
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
@ -47,16 +48,19 @@ def test_fiat_convert_is_supported():
|
|||||||
def test_fiat_convert_add_pair():
|
def test_fiat_convert_add_pair():
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
assert len(fiat_convert._pairs) == 0
|
pair_len = len(fiat_convert._pairs)
|
||||||
|
assert pair_len == 0
|
||||||
|
|
||||||
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
|
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
|
||||||
assert len(fiat_convert._pairs) == 1
|
pair_len = len(fiat_convert._pairs)
|
||||||
|
assert pair_len == 1
|
||||||
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||||
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||||
assert fiat_convert._pairs[0].price == 12345.0
|
assert fiat_convert._pairs[0].price == 12345.0
|
||||||
|
|
||||||
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
|
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
|
||||||
assert len(fiat_convert._pairs) == 2
|
pair_len = len(fiat_convert._pairs)
|
||||||
|
assert pair_len == 2
|
||||||
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
|
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
|
||||||
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
|
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
|
||||||
assert fiat_convert._pairs[1].price == 13000.2
|
assert fiat_convert._pairs[1].price == 13000.2
|
||||||
@ -67,12 +71,15 @@ def test_fiat_convert_find_price(mocker):
|
|||||||
'price_usd': 12345.0,
|
'price_usd': 12345.0,
|
||||||
'price_eur': 13000.2
|
'price_eur': 13000.2
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
|
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
||||||
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r'The crypto symbol XRP is not supported.'):
|
||||||
|
fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD')
|
||||||
|
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
|
||||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
||||||
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
|
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
|
||||||
@ -86,7 +93,7 @@ def test_fiat_convert_get_price(mocker):
|
|||||||
'price_usd': 28000.0,
|
'price_usd': 28000.0,
|
||||||
'price_eur': 15000.0
|
'price_eur': 15000.0
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
|
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()
|
||||||
@ -95,7 +102,8 @@ def test_fiat_convert_get_price(mocker):
|
|||||||
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
|
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
|
||||||
|
|
||||||
# Check the value return by the method
|
# Check the value return by the method
|
||||||
assert len(fiat_convert._pairs) == 0
|
pair_len = len(fiat_convert._pairs)
|
||||||
|
assert pair_len == 0
|
||||||
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
||||||
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||||
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||||
@ -116,10 +124,12 @@ def test_fiat_convert_get_price(mocker):
|
|||||||
assert fiat_convert._pairs[0]._expiration is not expiration
|
assert fiat_convert._pairs[0]._expiration is not expiration
|
||||||
|
|
||||||
|
|
||||||
def test_fiat_convert_without_network(mocker):
|
def test_fiat_convert_without_network():
|
||||||
pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!'))
|
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
|
||||||
mocker.patch('freqtrade.fiat_convert.Pymarketcap', pymarketcap)
|
|
||||||
|
|
||||||
fiat_convert = CryptoToFiatConverter()
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
CryptoToFiatConverter._coinmarketcap = None
|
||||||
|
|
||||||
assert fiat_convert._coinmarketcap is None
|
assert fiat_convert._coinmarketcap is None
|
||||||
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
|
||||||
|
1276
freqtrade/tests/test_freqtradebot.py
Normal file
1276
freqtrade/tests/test_freqtradebot.py
Normal file
File diff suppressed because it is too large
Load Diff
13
freqtrade/tests/test_indicator_helpers.py
Normal file
13
freqtrade/tests/test_indicator_helpers.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from freqtrade.indicator_helpers import went_up, went_down
|
||||||
|
|
||||||
|
|
||||||
|
def test_went_up():
|
||||||
|
series = pd.Series([1, 2, 3, 1])
|
||||||
|
assert went_up(series).equals(pd.Series([False, True, True, False]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_went_down():
|
||||||
|
series = pd.Series([1, 2, 3, 1])
|
||||||
|
assert went_down(series).equals(pd.Series([False, False, False, True]))
|
@ -1,31 +1,23 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
"""
|
||||||
import copy
|
Unit test file for main.py
|
||||||
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
|
|
||||||
import freqtrade.main as main
|
from freqtrade.main import main, set_loggers
|
||||||
from freqtrade import DependencyException, OperationalException
|
from freqtrade.tests.conftest import log_has
|
||||||
from freqtrade.analyze import SignalType
|
|
||||||
from freqtrade.exchange import Exchanges
|
|
||||||
from freqtrade.main import (_process, check_handle_timedout, create_trade,
|
|
||||||
execute_sell, get_target_bid, handle_trade, init)
|
|
||||||
from freqtrade.misc import State, get_state
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting(mocker):
|
def test_parse_args_backtesting(mocker) -> None:
|
||||||
""" Test that main() can start backtesting or hyperopt.
|
"""
|
||||||
and also ensure we can pass some specific arguments
|
Test that main() can start backtesting and also ensure we can pass some specific arguments
|
||||||
argument parsing is done in test_misc.py """
|
further argument parsing is done in test_arguments.py
|
||||||
backtesting_mock = mocker.patch(
|
"""
|
||||||
'freqtrade.optimize.backtesting.start', MagicMock())
|
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
|
||||||
with pytest.raises(SystemExit, match=r'0'):
|
main(['backtesting'])
|
||||||
main.main(['backtesting'])
|
|
||||||
assert backtesting_mock.call_count == 1
|
assert backtesting_mock.call_count == 1
|
||||||
call_args = backtesting_mock.call_args[0][0]
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
assert call_args.config == 'config.json'
|
assert call_args.config == 'config.json'
|
||||||
@ -33,14 +25,15 @@ def test_parse_args_backtesting(mocker):
|
|||||||
assert call_args.loglevel == 20
|
assert call_args.loglevel == 20
|
||||||
assert call_args.subparser == 'backtesting'
|
assert call_args.subparser == 'backtesting'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
assert call_args.ticker_interval == 5
|
assert call_args.ticker_interval is None
|
||||||
|
|
||||||
|
|
||||||
def test_main_start_hyperopt(mocker):
|
def test_main_start_hyperopt(mocker) -> None:
|
||||||
hyperopt_mock = mocker.patch(
|
"""
|
||||||
'freqtrade.optimize.hyperopt.start', MagicMock())
|
Test that main() can start hyperopt
|
||||||
with pytest.raises(SystemExit, match=r'0'):
|
"""
|
||||||
main.main(['hyperopt'])
|
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
||||||
|
main(['hyperopt'])
|
||||||
assert hyperopt_mock.call_count == 1
|
assert hyperopt_mock.call_count == 1
|
||||||
call_args = hyperopt_mock.call_args[0][0]
|
call_args = hyperopt_mock.call_args[0][0]
|
||||||
assert call_args.config == 'config.json'
|
assert call_args.config == 'config.json'
|
||||||
@ -49,628 +42,52 @@ def test_main_start_hyperopt(mocker):
|
|||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
|
def test_set_loggers() -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
Test set_loggers() update the logger level for third-party libraries
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
"""
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
previous_value1 = logging.getLogger('requests.packages.urllib3').level
|
||||||
validate_pairs=MagicMock(),
|
previous_value2 = logging.getLogger('telegram').level
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
set_loggers()
|
||||||
assert not trades
|
|
||||||
|
|
||||||
result = _process()
|
value1 = logging.getLogger('requests.packages.urllib3').level
|
||||||
assert result is True
|
assert previous_value1 is not value1
|
||||||
|
assert value1 is logging.INFO
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
value2 = logging.getLogger('telegram').level
|
||||||
assert len(trades) == 1
|
assert previous_value2 is not value2
|
||||||
trade = trades[0]
|
assert value2 is logging.INFO
|
||||||
assert trade is not None
|
|
||||||
assert trade.stake_amount == default_conf['stake_amount']
|
|
||||||
assert trade.is_open
|
|
||||||
assert trade.open_date is not None
|
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
|
||||||
assert trade.open_rate == 0.00001099
|
|
||||||
assert trade.amount == 90.99181073703367
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
def test_main(mocker, caplog) -> None:
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
"""
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
Test main() function
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
In this test we are skipping the while True loop by throwing an exception.
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
"""
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple(
|
||||||
validate_pairs=MagicMock(),
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
get_ticker=ticker,
|
_init_modules=MagicMock(),
|
||||||
get_wallet_health=health,
|
worker=MagicMock(
|
||||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
side_effect=KeyboardInterrupt
|
||||||
init(default_conf, create_engine('sqlite://'))
|
),
|
||||||
result = _process()
|
clean=MagicMock(),
|
||||||
assert result is False
|
|
||||||
assert sleep_mock.has_calls()
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_operational_exception(default_conf, ticker, health, mocker):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(side_effect=OperationalException))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
assert get_state() == State.RUNNING
|
|
||||||
|
|
||||||
result = _process()
|
|
||||||
assert result is False
|
|
||||||
assert get_state() == State.STOPPED
|
|
||||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal',
|
|
||||||
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_wallet_health=health,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert not trades
|
|
||||||
result = _process()
|
|
||||||
assert result is True
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
|
||||||
assert len(trades) == 1
|
|
||||||
|
|
||||||
result = _process()
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
# Save state of current whitelist
|
|
||||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade is not None
|
|
||||||
assert trade.stake_amount == 0.001
|
|
||||||
assert trade.is_open
|
|
||||||
assert trade.open_date is not None
|
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
|
|
||||||
assert trade.open_rate == 0.00001099
|
|
||||||
assert trade.amount == 90.99181073
|
|
||||||
|
|
||||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
buy_mock = mocker.patch(
|
|
||||||
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
|
|
||||||
)
|
)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
args = ['-c', 'config.json.example']
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
min_stake_amount = 0.0005
|
|
||||||
create_trade(min_stake_amount)
|
|
||||||
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
|
|
||||||
assert rate * amount >= min_stake_amount
|
|
||||||
|
|
||||||
|
# Test Main + the KeyboardInterrupt exception
|
||||||
|
with pytest.raises(SystemExit) as pytest_wrapped_e:
|
||||||
|
main(args)
|
||||||
|
log_has('Starting freqtrade', caplog.record_tuples)
|
||||||
|
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
|
||||||
|
assert pytest_wrapped_e.type == SystemExit
|
||||||
|
assert pytest_wrapped_e.value.code == 42
|
||||||
|
|
||||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
# Test the BaseException case
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch(
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
'freqtrade.freqtradebot.FreqtradeBot.worker',
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
MagicMock(side_effect=BaseException)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
|
||||||
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
|
|
||||||
create_trade(default_conf['stake_amount'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
|
|
||||||
conf = copy.deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'] = []
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
create_trade(default_conf['stake_amount'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
|
|
||||||
conf = copy.deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
|
|
||||||
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
|
||||||
create_trade(default_conf['stake_amount'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00001172,
|
|
||||||
'ask': 0.00001173,
|
|
||||||
'last': 0.00001172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
|
||||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
|
||||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert trade.is_open is True
|
|
||||||
|
|
||||||
handle_trade(trade)
|
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
|
|
||||||
assert trade.close_rate == 0.00001173
|
|
||||||
assert trade.close_profit == 0.06201057
|
|
||||||
assert trade.calc_profit() == 0.00006217
|
|
||||||
assert trade.close_date is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
|
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.is_open = True
|
|
||||||
|
|
||||||
# FIX: sniffing logs, suggest handle_trade should not execute_sell
|
|
||||||
# instead that responsibility should be moved out of handle_trade(),
|
|
||||||
# we might just want to check if we are in a sell condition without
|
|
||||||
# executing
|
|
||||||
# if ROI is reached we must sell
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
|
||||||
assert handle_trade(trade)
|
|
||||||
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
|
||||||
# if ROI is reached we must sell even if sell-signal is not signalled
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
assert handle_trade(trade)
|
|
||||||
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
|
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.is_open = True
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
|
||||||
value_returned = handle_trade(trade)
|
|
||||||
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
|
|
||||||
assert value_returned is False
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
assert handle_trade(trade)
|
|
||||||
s = 'Executing sell due to sell signal ...'
|
|
||||||
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
|
|
||||||
|
|
||||||
|
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
# Create trade and sell it
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
assert trade.is_open is False
|
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
|
||||||
handle_trade(trade)
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_buy = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=True
|
|
||||||
)
|
)
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
Trade.session.add(trade_buy)
|
main(args)
|
||||||
|
log_has('Got fatal exception!', caplog.record_tuples)
|
||||||
# check it does cancel buy orders over the time limit
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
|
||||||
assert len(trades) == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_sell = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(hours=-5).datetime,
|
|
||||||
close_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=False
|
|
||||||
)
|
|
||||||
|
|
||||||
Trade.session.add(trade_sell)
|
|
||||||
|
|
||||||
# check it does cancel sell orders over the time limit
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
assert trade_sell.is_open is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
|
||||||
mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
|
||||||
cancel_order=cancel_order_mock)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
trade_buy = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
open_rate=0.00001099,
|
|
||||||
exchange='BITTREX',
|
|
||||||
open_order_id='123456789',
|
|
||||||
amount=90.99181073,
|
|
||||||
fee=0.0,
|
|
||||||
stake_amount=1,
|
|
||||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
|
||||||
is_open=True
|
|
||||||
)
|
|
||||||
|
|
||||||
Trade.session.add(trade_buy)
|
|
||||||
|
|
||||||
# check it does cancel buy orders over the time limit
|
|
||||||
# note this is for a partially-complete buy order
|
|
||||||
check_handle_timedout(600)
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
|
||||||
assert len(trades) == 1
|
|
||||||
assert trades[0].amount == 23.0
|
|
||||||
assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
|
||||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
|
||||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Increase the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up)
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
|
||||||
_CONF=default_conf,
|
|
||||||
init=MagicMock(),
|
|
||||||
send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
|
||||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Decrease the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down)
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker)
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
assert trade
|
|
||||||
|
|
||||||
# Increase the price and sell it
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up)
|
|
||||||
mocker.patch('freqtrade.main._CONF', {})
|
|
||||||
|
|
||||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
|
||||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert handle_trade(trade) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert handle_trade(trade) is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert handle_trade(trade) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
|
||||||
default_conf['experimental'] = {
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
}
|
|
||||||
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
|
||||||
create_trade(0.001)
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
assert handle_trade(trade) is True
|
|
||||||
|
@ -1,164 +1,71 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import pytest
|
"""
|
||||||
from jsonschema import ValidationError
|
Unit test file for misc.py
|
||||||
|
"""
|
||||||
|
|
||||||
from freqtrade.misc import (common_args_parser, load_config, parse_args,
|
import datetime
|
||||||
throttle)
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
||||||
|
common_datearray, file_dump_json)
|
||||||
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
|
||||||
|
|
||||||
def test_throttle():
|
def test_shorten_date() -> None:
|
||||||
|
"""
|
||||||
def func():
|
Test shorten_date() function
|
||||||
return 42
|
:return: None
|
||||||
|
"""
|
||||||
start = time.time()
|
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
||||||
result = throttle(func, min_secs=0.1)
|
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
||||||
end = time.time()
|
assert shorten_date(str_data) == str_shorten_data
|
||||||
|
|
||||||
assert result == 42
|
|
||||||
assert end - start > 0.1
|
|
||||||
|
|
||||||
result = throttle(func, min_secs=-1)
|
|
||||||
assert result == 42
|
|
||||||
|
|
||||||
|
|
||||||
def test_throttle_with_assets():
|
def test_datesarray_to_datetimearray(ticker_history):
|
||||||
|
"""
|
||||||
|
Test datesarray_to_datetimearray() function
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
|
||||||
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
def func(nb_assets=-1):
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
return nb_assets
|
assert dates[0].year == 2017
|
||||||
|
assert dates[0].month == 11
|
||||||
|
assert dates[0].day == 26
|
||||||
|
assert dates[0].hour == 8
|
||||||
|
assert dates[0].minute == 50
|
||||||
|
|
||||||
result = throttle(func, min_secs=0.1, nb_assets=666)
|
date_len = len(dates)
|
||||||
assert result == 666
|
assert date_len == 3
|
||||||
|
|
||||||
result = throttle(func, min_secs=0.1)
|
|
||||||
assert result == -1
|
|
||||||
|
|
||||||
|
|
||||||
# Parse common command-line-arguments. Used for all tools
|
def test_common_datearray(default_conf, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Test common_datearray()
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
analyze = Analyze(default_conf)
|
||||||
|
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
|
||||||
|
tickerlist = {'BTC_UNITEST': tick}
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
def test_parse_args_none():
|
dates = common_datearray(dataframes)
|
||||||
args = common_args_parser('')
|
|
||||||
assert isinstance(args, argparse.ArgumentParser)
|
assert dates.size == dataframes['BTC_UNITEST']['date'].size
|
||||||
|
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
|
||||||
|
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_defaults():
|
def test_file_dump_json(mocker) -> None:
|
||||||
args = parse_args([], '')
|
"""
|
||||||
assert args.config == 'config.json'
|
Test file_dump_json()
|
||||||
assert args.dynamic_whitelist is None
|
:return: None
|
||||||
assert args.loglevel == 20
|
"""
|
||||||
|
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||||
|
json_dump = mocker.patch('json.dump', MagicMock())
|
||||||
def test_parse_args_config():
|
file_dump_json('somefile', [1, 2, 3])
|
||||||
args = parse_args(['-c', '/dev/null'], '')
|
assert file_open.call_count == 1
|
||||||
assert args.config == '/dev/null'
|
assert json_dump.call_count == 1
|
||||||
|
|
||||||
args = parse_args(['--config', '/dev/null'], '')
|
|
||||||
assert args.config == '/dev/null'
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_verbose():
|
|
||||||
args = parse_args(['-v'], '')
|
|
||||||
assert args.loglevel == 10
|
|
||||||
|
|
||||||
args = parse_args(['--verbose'], '')
|
|
||||||
assert args.loglevel == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_version():
|
|
||||||
with pytest.raises(SystemExit, match=r'0'):
|
|
||||||
parse_args(['--version'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_invalid():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['-c'], '')
|
|
||||||
|
|
||||||
|
|
||||||
# Parse command-line-arguments
|
|
||||||
# used for main, backtesting and hyperopt
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist():
|
|
||||||
args = parse_args(['--dynamic-whitelist'], '')
|
|
||||||
assert args.dynamic_whitelist == 20
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist_10():
|
|
||||||
args = parse_args(['--dynamic-whitelist', '10'], '')
|
|
||||||
assert args.dynamic_whitelist == 10
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_dynamic_whitelist_invalid_values():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['--dynamic-whitelist', 'abc'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_invalid():
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['backtesting --ticker-interval'], '')
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit, match=r'2'):
|
|
||||||
parse_args(['backtesting --ticker-interval', 'abc'], '')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_custom():
|
|
||||||
args = [
|
|
||||||
'-c', 'test_conf.json',
|
|
||||||
'backtesting',
|
|
||||||
'--live',
|
|
||||||
'--ticker-interval', '1',
|
|
||||||
'--refresh-pairs-cached']
|
|
||||||
call_args = parse_args(args, '')
|
|
||||||
assert call_args.config == 'test_conf.json'
|
|
||||||
assert call_args.live is True
|
|
||||||
assert call_args.loglevel == 20
|
|
||||||
assert call_args.subparser == 'backtesting'
|
|
||||||
assert call_args.func is not None
|
|
||||||
assert call_args.ticker_interval == 1
|
|
||||||
assert call_args.refresh_pairs is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_hyperopt_custom(mocker):
|
|
||||||
args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
|
|
||||||
call_args = parse_args(args, '')
|
|
||||||
assert call_args.config == 'test_conf.json'
|
|
||||||
assert call_args.epochs == 20
|
|
||||||
assert call_args.loglevel == 20
|
|
||||||
assert call_args.subparser == 'hyperopt'
|
|
||||||
assert call_args.func is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config(default_conf, mocker):
|
|
||||||
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
|
|
||||||
read_data=json.dumps(default_conf)
|
|
||||||
))
|
|
||||||
validated_conf = load_config('somefile')
|
|
||||||
assert file_mock.call_count == 1
|
|
||||||
assert validated_conf.items() >= default_conf.items()
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_invalid_pair(default_conf, mocker):
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.misc.open',
|
|
||||||
mocker.mock_open(
|
|
||||||
read_data=json.dumps(conf)))
|
|
||||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
|
||||||
load_config('somefile')
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_missing_attributes(default_conf, mocker):
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.pop('exchange')
|
|
||||||
mocker.patch(
|
|
||||||
'freqtrade.misc.open',
|
|
||||||
mocker.mock_open(
|
|
||||||
read_data=json.dumps(conf)))
|
|
||||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
|
||||||
load_config('somefile')
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.persistence import Trade, init
|
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def init_persistence(default_conf):
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_init_create_session(default_conf, mocker):
|
def test_init_create_session(default_conf, mocker):
|
||||||
@ -13,7 +19,7 @@ def test_init_create_session(default_conf, mocker):
|
|||||||
# Check if init create a session
|
# Check if init create a session
|
||||||
init(default_conf)
|
init(default_conf)
|
||||||
assert hasattr(Trade, 'session')
|
assert hasattr(Trade, 'session')
|
||||||
assert type(Trade.session).__name__ is 'Session'
|
assert 'Session' in type(Trade.session).__name__
|
||||||
|
|
||||||
|
|
||||||
def test_init_dry_run_db(default_conf, mocker):
|
def test_init_dry_run_db(default_conf, mocker):
|
||||||
@ -89,6 +95,7 @@ def test_init_prod_db(default_conf, mocker):
|
|||||||
os.rename(prod_db_swp, prod_db)
|
os.rename(prod_db_swp, prod_db)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
||||||
"""
|
"""
|
||||||
On this test we will buy and sell a crypto currency.
|
On this test we will buy and sell a crypto currency.
|
||||||
@ -143,6 +150,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -165,6 +173,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
|||||||
assert trade.calc_profit_percent() == 0.06201057
|
assert trade.calc_profit_percent() == 0.06201057
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_close_trade_price_exception(limit_buy_order):
|
def test_calc_close_trade_price_exception(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -178,6 +187,7 @@ def test_calc_close_trade_price_exception(limit_buy_order):
|
|||||||
assert trade.calc_close_trade_price() == 0.0
|
assert trade.calc_close_trade_price() == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_open_order(limit_buy_order):
|
def test_update_open_order(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -200,6 +210,7 @@ def test_update_open_order(limit_buy_order):
|
|||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_update_invalid_order(limit_buy_order):
|
def test_update_invalid_order(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -212,6 +223,7 @@ def test_update_invalid_order(limit_buy_order):
|
|||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_open_trade_price(limit_buy_order):
|
def test_calc_open_trade_price(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -229,6 +241,7 @@ def test_calc_open_trade_price(limit_buy_order):
|
|||||||
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
|
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -250,6 +263,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
|||||||
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
|
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_profit(limit_buy_order, limit_sell_order):
|
def test_calc_profit(limit_buy_order, limit_sell_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -272,10 +286,6 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
|
|||||||
# Lower than open rate
|
# Lower than open rate
|
||||||
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
|
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
|
||||||
|
|
||||||
# Only custom fee without sell order applied
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
trade.calc_profit(fee=0.003)
|
|
||||||
|
|
||||||
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_profit() == 0.00006217
|
assert trade.calc_profit() == 0.00006217
|
||||||
@ -284,6 +294,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
|
|||||||
assert trade.calc_profit(fee=0.003) == 0.00006163
|
assert trade.calc_profit(fee=0.003) == 0.00006163
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -300,13 +311,56 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
|||||||
# Get percent of profit with a custom rate (Lower than open rate)
|
# Get percent of profit with a custom rate (Lower than open rate)
|
||||||
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
|
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
|
||||||
|
|
||||||
# Only custom fee without sell order applied
|
|
||||||
with pytest.raises(TypeError):
|
|
||||||
trade.calc_profit_percent(fee=0.003)
|
|
||||||
|
|
||||||
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.calc_profit_percent() == 0.06201057
|
assert trade.calc_profit_percent() == 0.06201057
|
||||||
|
|
||||||
# Test with a custom fee rate on the close trade
|
# Test with a custom fee rate on the close trade
|
||||||
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
||||||
|
|
||||||
|
|
||||||
|
def test_clean_dry_run_db(default_conf):
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Simulate dry_run entries
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee=0.0025,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='BITTREX',
|
||||||
|
open_order_id='dry_run_buy_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee=0.0025,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='BITTREX',
|
||||||
|
open_order_id='dry_run_sell_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
# Simulate prod entry
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee=0.0025,
|
||||||
|
open_rate=0.123,
|
||||||
|
exchange='BITTREX',
|
||||||
|
open_order_id='prod_buy_12345'
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
# We have 3 entries: 2 dry_run, 1 prod
|
||||||
|
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3
|
||||||
|
|
||||||
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
# We have now only the prod
|
||||||
|
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
|
||||||
|
14
freqtrade/tests/test_state.py
Normal file
14
freqtrade/tests/test_state.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Unit test file for constants.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from freqtrade.state import State
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_object() -> None:
|
||||||
|
"""
|
||||||
|
Test the State object has the mandatory states
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
assert hasattr(State, 'RUNNING')
|
||||||
|
assert hasattr(State, 'STOPPED')
|
2
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
2
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
2
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
2
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
2
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
2
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
2
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
2
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
2
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
2
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
2
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
2
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
2
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
2
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
2
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_UNITEST-30.json
vendored
Normal file
File diff suppressed because one or more lines are too long
3
freqtrade/tests/testdata/BTC_UNITEST-8.json
vendored
Normal file
3
freqtrade/tests/testdata/BTC_UNITEST-8.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
{"O": 0.00162008, "H": 0.00162008, "L": 0.00162008, "C": 0.00162008, "V": 108.14853839, "T": "2017-11-04T23:02:00", "BV": 0.17520927}
|
||||||
|
]
|
BIN
freqtrade/tests/testdata/BTC_UNITEST-8.json.gz
vendored
Normal file
BIN
freqtrade/tests/testdata/BTC_UNITEST-8.json.gz
vendored
Normal file
Binary file not shown.
2
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
2
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
2
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
2
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
2
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
2
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
2
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
File diff suppressed because one or more lines are too long
@ -2,28 +2,37 @@
|
|||||||
|
|
||||||
"""This script generate json data from bittrex"""
|
"""This script generate json data from bittrex"""
|
||||||
import json
|
import json
|
||||||
from os import path
|
import sys
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
|
from freqtrade import misc
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
PAIRS = [
|
parser = misc.common_args_parser('download utility')
|
||||||
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
|
parser.add_argument(
|
||||||
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
|
'-p', '--pair',
|
||||||
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
|
help='JSON file containing pairs to download',
|
||||||
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
|
dest='pair',
|
||||||
]
|
default=None
|
||||||
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
|
)
|
||||||
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
args = parser.parse_args(sys.argv[1:])
|
||||||
|
|
||||||
|
TICKER_INTERVALS = [1, 5] # ticker interval in minutes (currently implemented: 1 and 5)
|
||||||
|
PAIRS = []
|
||||||
|
|
||||||
|
if args.pair:
|
||||||
|
with open(args.pair) as file:
|
||||||
|
PAIRS = json.load(file)
|
||||||
|
PAIRS = list(set(PAIRS))
|
||||||
|
|
||||||
|
print('About to download pairs:', PAIRS)
|
||||||
|
|
||||||
# Init Bittrex exchange
|
# Init Bittrex exchange
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
for tick_interval in TICKER_INTERVALS:
|
||||||
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
|
print('downloading pair %s, interval %s' % (pair, tick_interval))
|
||||||
pair,
|
data = exchange.get_ticker_history(pair, tick_interval)
|
||||||
TICKER_INTERVAL,
|
filename = '{}-{}.json'.format(pair, tick_interval)
|
||||||
))
|
misc.file_dump_json(filename, data)
|
||||||
with open(filename, 'w') as fp:
|
|
||||||
json.dump(data, fp)
|
|
||||||
|
26
freqtrade/tests/testdata/pairs.json
vendored
Normal file
26
freqtrade/tests/testdata/pairs.json
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
"BTC_ADA",
|
||||||
|
"BTC_BAT",
|
||||||
|
"BTC_DASH",
|
||||||
|
"BTC_ETC",
|
||||||
|
"BTC_ETH",
|
||||||
|
"BTC_GBYTE",
|
||||||
|
"BTC_LSK",
|
||||||
|
"BTC_LTC",
|
||||||
|
"BTC_NEO",
|
||||||
|
"BTC_NXT",
|
||||||
|
"BTC_POWR",
|
||||||
|
"BTC_STORJ",
|
||||||
|
"BTC_QTUM",
|
||||||
|
"BTC_WAVES",
|
||||||
|
"BTC_VTC",
|
||||||
|
"BTC_XLM",
|
||||||
|
"BTC_XMR",
|
||||||
|
"BTC_XVG",
|
||||||
|
"BTC_XRP",
|
||||||
|
"BTC_ZEC",
|
||||||
|
"USDT_BTC",
|
||||||
|
"USDT_LTC",
|
||||||
|
"USDT_ETH"
|
||||||
|
]
|
||||||
|
|
@ -1,26 +1,25 @@
|
|||||||
python-bittrex==0.2.2
|
python-bittrex==0.3.0
|
||||||
SQLAlchemy==1.2.0
|
SQLAlchemy==1.2.7
|
||||||
python-telegram-bot==9.0.0
|
python-telegram-bot==10.1.0
|
||||||
arrow==0.12.0
|
arrow==0.12.1
|
||||||
cachetools==2.0.1
|
cachetools==2.0.1
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.22.0
|
pandas==0.22.0
|
||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.1
|
||||||
scipy==1.0.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.14.0
|
numpy==1.14.3
|
||||||
TA-Lib==0.4.15
|
TA-Lib==0.4.17
|
||||||
pytest==3.3.2
|
pytest==3.5.1
|
||||||
pytest-mock==1.6.3
|
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
|
networkx==1.11
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
pymarketcap==3.3.147
|
coinmarketcap==5.0.1
|
||||||
|
|
||||||
# Required for plotting data
|
# Required for plotting data
|
||||||
#matplotlib==2.1.0
|
#plotly==2.3.0
|
||||||
#PYQT5==5.9
|
|
||||||
|
@ -1,70 +1,182 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to display when the bot will buy a specific pair
|
||||||
|
|
||||||
|
Mandatory Cli parameters:
|
||||||
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
|
Optional Cli parameters
|
||||||
|
-s / --strategy: strategy to use
|
||||||
|
-d / --datadir: path to pair backtest data
|
||||||
|
--timerange: specify what timerange of data to use.
|
||||||
|
-l / --live: Live, to download the latest ticker for the pair
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
from argparse import Namespace
|
||||||
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
|
||||||
matplotlib.use("Qt5Agg")
|
from typing import List
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from freqtrade import exchange, analyze
|
from plotly import tools
|
||||||
from freqtrade.misc import common_args_parser
|
from plotly.offline import plot
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
from freqtrade import exchange
|
||||||
|
import freqtrade.optimize as optimize
|
||||||
|
|
||||||
|
|
||||||
def plot_parse_args(args ):
|
logger = logging.getLogger(__name__)
|
||||||
parser = common_args_parser(description='Graph utility')
|
|
||||||
parser.add_argument(
|
|
||||||
'-p', '--pair',
|
|
||||||
help = 'What currency pair',
|
|
||||||
dest = 'pair',
|
|
||||||
default = 'BTC_ETH',
|
|
||||||
type = str,
|
|
||||||
)
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args) -> None:
|
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
Calls analyze() and plots the returned dataframe
|
Calls analyze() and plots the returned dataframe
|
||||||
:param pair: pair as str
|
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
pair = args.pair
|
pair = args.pair.replace('-', '_')
|
||||||
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
# Init Bittrex to use public API
|
# Init strategy
|
||||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
try:
|
||||||
ticker = exchange.get_ticker_history(pair)
|
analyze = Analyze({'strategy': args.strategy})
|
||||||
dataframe = analyze.analyze_ticker(ticker)
|
except AttributeError:
|
||||||
|
logger.critical(
|
||||||
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
|
args.strategy
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
tick_interval = analyze.strategy.ticker_interval
|
||||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
|
||||||
|
|
||||||
# Two subplots sharing x axis
|
tickers = {}
|
||||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
if args.live:
|
||||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
logger.info('Downloading pair.')
|
||||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
# Init Bittrex to use public API
|
||||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||||
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
||||||
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
else:
|
||||||
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
tickers = optimize.load_data(
|
||||||
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
datadir=args.datadir,
|
||||||
ax1.legend()
|
pairs=[pair],
|
||||||
|
ticker_interval=tick_interval,
|
||||||
|
refresh_pairs=False,
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
|
dataframe = dataframes[pair]
|
||||||
|
dataframe = analyze.populate_buy_trend(dataframe)
|
||||||
|
dataframe = analyze.populate_sell_trend(dataframe)
|
||||||
|
|
||||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
if len(dataframe.index) > 750:
|
||||||
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
|
logger.warning('Ticker contained more than 750 candles, clipping.')
|
||||||
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
data = dataframe.tail(750)
|
||||||
ax2.legend()
|
|
||||||
|
|
||||||
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
|
candles = go.Candlestick(
|
||||||
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
|
x=data.date,
|
||||||
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
open=data.open,
|
||||||
ax3.legend()
|
high=data.high,
|
||||||
|
low=data.low,
|
||||||
|
close=data.close,
|
||||||
|
name='Price'
|
||||||
|
)
|
||||||
|
|
||||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
df_buy = data[data['buy'] == 1]
|
||||||
# all but bottom plot.
|
buys = go.Scattergl(
|
||||||
fig.subplots_adjust(hspace=0)
|
x=df_buy.date,
|
||||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
y=df_buy.close,
|
||||||
plt.show()
|
mode='markers',
|
||||||
|
name='buy',
|
||||||
|
marker=dict(
|
||||||
|
symbol='triangle-up-dot',
|
||||||
|
size=9,
|
||||||
|
line=dict(width=1),
|
||||||
|
color='green',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
df_sell = data[data['sell'] == 1]
|
||||||
|
sells = go.Scattergl(
|
||||||
|
x=df_sell.date,
|
||||||
|
y=df_sell.close,
|
||||||
|
mode='markers',
|
||||||
|
name='sell',
|
||||||
|
marker=dict(
|
||||||
|
symbol='triangle-down-dot',
|
||||||
|
size=9,
|
||||||
|
line=dict(width=1),
|
||||||
|
color='red',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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"},
|
||||||
|
)
|
||||||
|
macd = go.Scattergl(x=data['date'], y=data['macd'], name='MACD')
|
||||||
|
macdsignal = go.Scattergl(x=data['date'], y=data['macdsignal'], name='MACD signal')
|
||||||
|
volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
|
||||||
|
|
||||||
|
fig = tools.make_subplots(
|
||||||
|
rows=3,
|
||||||
|
cols=1,
|
||||||
|
shared_xaxes=True,
|
||||||
|
row_width=[1, 1, 4],
|
||||||
|
vertical_spacing=0.0001,
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.append_trace(candles, 1, 1)
|
||||||
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
|
fig.append_trace(buys, 1, 1)
|
||||||
|
fig.append_trace(sells, 1, 1)
|
||||||
|
fig.append_trace(volume, 2, 1)
|
||||||
|
fig.append_trace(macd, 3, 1)
|
||||||
|
fig.append_trace(macdsignal, 3, 1)
|
||||||
|
|
||||||
|
fig['layout'].update(title=args.pair)
|
||||||
|
fig['layout']['yaxis1'].update(title='Price')
|
||||||
|
fig['layout']['yaxis2'].update(title='Volume')
|
||||||
|
fig['layout']['yaxis3'].update(title='MACD')
|
||||||
|
|
||||||
|
plot(fig, filename='freqtrade-plot.html')
|
||||||
|
|
||||||
|
|
||||||
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse args passed to the script
|
||||||
|
:param args: Cli arguments
|
||||||
|
:return: args: Array with all arguments
|
||||||
|
"""
|
||||||
|
arguments = Arguments(args, 'Graph dataframe')
|
||||||
|
arguments.scripts_options()
|
||||||
|
arguments.common_args_parser()
|
||||||
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
|
||||||
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargv: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
This function will initiate the bot and start the trading loop.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info('Starting Plot Dataframe')
|
||||||
|
plot_analyzed_dataframe(
|
||||||
|
plot_parse_args(sysargv)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = plot_parse_args(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
plot_analyzed_dataframe(args)
|
|
||||||
|
223
scripts/plot_profit.py
Executable file
223
scripts/plot_profit.py
Executable file
@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to display profits
|
||||||
|
|
||||||
|
Mandatory Cli parameters:
|
||||||
|
-p / --pair: pair to examine
|
||||||
|
|
||||||
|
Optional Cli parameters
|
||||||
|
-c / --config: specify configuration file
|
||||||
|
-s / --strategy: strategy to use
|
||||||
|
--timerange: specify what timerange of data to use.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import List, Optional
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from plotly import tools
|
||||||
|
from plotly.offline import plot
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.analyze import Analyze
|
||||||
|
|
||||||
|
import freqtrade.optimize as optimize
|
||||||
|
import freqtrade.misc as misc
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# data:: [ pair, profit-%, enter, exit, time, duration]
|
||||||
|
# data:: ["BTC_ETH", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
|
||||||
|
def make_profit_array(
|
||||||
|
data: List, px: int, min_date: int,
|
||||||
|
interval: int, filter_pairs: Optional[List] = None) -> np.ndarray:
|
||||||
|
pg = np.zeros(px)
|
||||||
|
filter_pairs = filter_pairs or []
|
||||||
|
# Go through the trades
|
||||||
|
# and make an total profit
|
||||||
|
# array
|
||||||
|
for trade in data:
|
||||||
|
pair = trade[0]
|
||||||
|
if filter_pairs and pair not in filter_pairs:
|
||||||
|
continue
|
||||||
|
profit = trade[1]
|
||||||
|
trade_sell_time = int(trade[3])
|
||||||
|
|
||||||
|
ix = define_index(min_date, trade_sell_time, interval)
|
||||||
|
if ix < px:
|
||||||
|
logger.debug('[%s]: Add profit %s on %s', pair, profit, trade[4])
|
||||||
|
pg[ix] += profit
|
||||||
|
|
||||||
|
# rewrite the pg array to go from
|
||||||
|
# total profits at each timeframe
|
||||||
|
# to accumulated profits
|
||||||
|
pa = 0
|
||||||
|
for x in range(0, len(pg)):
|
||||||
|
p = pg[x] # Get current total percent
|
||||||
|
pa += p # Add to the accumulated percent
|
||||||
|
pg[x] = pa # write back to save memory
|
||||||
|
|
||||||
|
return pg
|
||||||
|
|
||||||
|
|
||||||
|
def plot_profit(args: Namespace) -> None:
|
||||||
|
"""
|
||||||
|
Plots the total profit for all pairs.
|
||||||
|
Note, the profit calculation isn't realistic.
|
||||||
|
But should be somewhat proportional, and therefor useful
|
||||||
|
in helping out to find a good algorithm.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We need to use the same pairs, same tick_interval
|
||||||
|
# and same timeperiod as used in backtesting
|
||||||
|
# to match the tickerdata against the profits-results
|
||||||
|
timerange = Arguments.parse_timerange(args.timerange)
|
||||||
|
|
||||||
|
config = Configuration(args).get_config()
|
||||||
|
|
||||||
|
# Init strategy
|
||||||
|
try:
|
||||||
|
analyze = Analyze({'strategy': config.get('strategy')})
|
||||||
|
except AttributeError:
|
||||||
|
logger.critical(
|
||||||
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
|
config.get('strategy')
|
||||||
|
)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Take pairs from the cli otherwise switch to the pair in the config file
|
||||||
|
if args.pair:
|
||||||
|
filter_pairs = args.pair
|
||||||
|
filter_pairs = filter_pairs.split(',')
|
||||||
|
else:
|
||||||
|
filter_pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
tick_interval = analyze.strategy.ticker_interval
|
||||||
|
pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
if filter_pairs:
|
||||||
|
pairs = list(set(pairs) & set(filter_pairs))
|
||||||
|
logger.info('Filter, keep pairs %s' % pairs)
|
||||||
|
|
||||||
|
tickers = optimize.load_data(
|
||||||
|
datadir=args.datadir,
|
||||||
|
pairs=pairs,
|
||||||
|
ticker_interval=tick_interval,
|
||||||
|
refresh_pairs=False,
|
||||||
|
timerange=timerange
|
||||||
|
)
|
||||||
|
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
||||||
|
|
||||||
|
# NOTE: the dataframes are of unequal length,
|
||||||
|
# 'dates' is an merged date array of them all.
|
||||||
|
|
||||||
|
dates = misc.common_datearray(dataframes)
|
||||||
|
min_date = int(min(dates).timestamp())
|
||||||
|
max_date = int(max(dates).timestamp())
|
||||||
|
num_iterations = define_index(min_date, max_date, tick_interval) + 1
|
||||||
|
|
||||||
|
# Make an average close price of all the pairs that was involved.
|
||||||
|
# this could be useful to gauge the overall market trend
|
||||||
|
# We are essentially saying:
|
||||||
|
# array <- sum dataframes[*]['close'] / num_items dataframes
|
||||||
|
# FIX: there should be some onliner numpy/panda for this
|
||||||
|
avgclose = np.zeros(num_iterations)
|
||||||
|
num = 0
|
||||||
|
for pair, pair_data in dataframes.items():
|
||||||
|
close = pair_data['close']
|
||||||
|
maxprice = max(close) # Normalize price to [0,1]
|
||||||
|
logger.info('Pair %s has length %s' % (pair, len(close)))
|
||||||
|
for x in range(0, len(close)):
|
||||||
|
avgclose[x] += close[x] / maxprice
|
||||||
|
# avgclose += close
|
||||||
|
num += 1
|
||||||
|
avgclose /= num
|
||||||
|
|
||||||
|
# Load the profits results
|
||||||
|
# And make an profits-growth array
|
||||||
|
|
||||||
|
try:
|
||||||
|
filename = 'backtest-result.json'
|
||||||
|
with open(filename) as file:
|
||||||
|
data = json.load(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.critical('File "backtest-result.json" not found. This script require backtesting '
|
||||||
|
'results to run.\nPlease run a backtesting with the parameter --export.')
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Plot the pairs average close prices, and total profit growth
|
||||||
|
#
|
||||||
|
|
||||||
|
avgclose = go.Scattergl(
|
||||||
|
x=dates,
|
||||||
|
y=avgclose,
|
||||||
|
name='Avg close price',
|
||||||
|
)
|
||||||
|
|
||||||
|
profit = go.Scattergl(
|
||||||
|
x=dates,
|
||||||
|
y=pg,
|
||||||
|
name='Profit',
|
||||||
|
)
|
||||||
|
|
||||||
|
fig = tools.make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1])
|
||||||
|
|
||||||
|
fig.append_trace(avgclose, 1, 1)
|
||||||
|
fig.append_trace(profit, 2, 1)
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
pg = make_profit_array(data, num_iterations, min_date, tick_interval, pair)
|
||||||
|
pair_profit = go.Scattergl(
|
||||||
|
x=dates,
|
||||||
|
y=pg,
|
||||||
|
name=pair,
|
||||||
|
)
|
||||||
|
fig.append_trace(pair_profit, 3, 1)
|
||||||
|
|
||||||
|
plot(fig, filename='freqtrade-profit-plot.html')
|
||||||
|
|
||||||
|
|
||||||
|
def define_index(min_date: int, max_date: int, interval: int) -> int:
|
||||||
|
"""
|
||||||
|
Return the index of a specific date
|
||||||
|
"""
|
||||||
|
return int((max_date - min_date) / (interval * 60))
|
||||||
|
|
||||||
|
|
||||||
|
def plot_parse_args(args: List[str]) -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse args passed to the script
|
||||||
|
:param args: Cli arguments
|
||||||
|
:return: args: Array with all arguments
|
||||||
|
"""
|
||||||
|
arguments = Arguments(args, 'Graph profits')
|
||||||
|
arguments.scripts_options()
|
||||||
|
arguments.common_args_parser()
|
||||||
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
|
||||||
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main(sysargv: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
This function will initiate the bot and start the trading loop.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info('Starting Plot Dataframe')
|
||||||
|
plot_profit(
|
||||||
|
plot_parse_args(sysargv)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main(sys.argv[1:])
|
2
setup.py
2
setup.py
@ -35,7 +35,7 @@ setup(name='freqtrade',
|
|||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'tabulate',
|
'tabulate',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'pymarketcap',
|
'coinmarketcap',
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
208
setup.sh
Executable file
208
setup.sh
Executable file
@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#encoding=utf8
|
||||||
|
|
||||||
|
function updateenv () {
|
||||||
|
echo "
|
||||||
|
-------------------------
|
||||||
|
Update your virtual env
|
||||||
|
-------------------------
|
||||||
|
"
|
||||||
|
source .env/bin/activate
|
||||||
|
pip3.6 install --upgrade pip
|
||||||
|
pip3 install -r requirements.txt --upgrade
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
pip3 install -e .
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install tab lib
|
||||||
|
function install_talib () {
|
||||||
|
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||||
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
|
cd ta-lib && ./configure --prefix=/usr && make && sudo make install
|
||||||
|
cd .. && rm -rf ./ta-lib*
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install bot MacOS
|
||||||
|
function install_macos () {
|
||||||
|
if [ ! -x "$(command -v brew)" ]
|
||||||
|
then
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Install Brew"
|
||||||
|
echo "-------------------------"
|
||||||
|
echo
|
||||||
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
|
fi
|
||||||
|
brew install python3 wget ta-lib
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install bot Debian_ubuntu
|
||||||
|
function install_debian () {
|
||||||
|
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
||||||
|
install_talib
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upgrade the bot
|
||||||
|
function update () {
|
||||||
|
git pull
|
||||||
|
updateenv
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reset Develop or Master branch
|
||||||
|
function reset () {
|
||||||
|
echo "----------------------------"
|
||||||
|
echo "Reset branch and virtual env"
|
||||||
|
echo "----------------------------"
|
||||||
|
echo
|
||||||
|
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
|
||||||
|
then
|
||||||
|
if [ -d ".env" ]; then
|
||||||
|
echo "- Delete your previous virtual env"
|
||||||
|
rm -rf .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
git fetch -a
|
||||||
|
|
||||||
|
if [ "1" == $(git branch -vv |grep -c "* develop") ]
|
||||||
|
then
|
||||||
|
echo "- Hard resetting of 'develop' branch."
|
||||||
|
git reset --hard origin/develop
|
||||||
|
elif [ "1" == $(git branch -vv |grep -c "* master") ]
|
||||||
|
then
|
||||||
|
echo "- Hard resetting of 'master' branch."
|
||||||
|
git reset --hard origin/master
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Reset ignored because you are not on 'master' or 'develop'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3.6 -m venv .env
|
||||||
|
updateenv
|
||||||
|
}
|
||||||
|
|
||||||
|
function config_generator () {
|
||||||
|
|
||||||
|
echo "Starting to generate config.json"
|
||||||
|
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "General configuration"
|
||||||
|
echo "-------------------------"
|
||||||
|
echo
|
||||||
|
read -p "Max open trades: (Default: 3) " max_trades
|
||||||
|
|
||||||
|
read -p "Stake amount: (Default: 0.05) " stake_amount
|
||||||
|
|
||||||
|
read -p "Stake currency: (Default: BTC) " stake_currency
|
||||||
|
|
||||||
|
read -p "Fiat currency: (Default: USD) " fiat_currency
|
||||||
|
|
||||||
|
echo "------------------------"
|
||||||
|
echo "Bittrex config generator"
|
||||||
|
echo "------------------------"
|
||||||
|
echo
|
||||||
|
read -p "Exchange API key: " api_key
|
||||||
|
read -p "Exchange API Secret: " api_secret
|
||||||
|
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Telegram config generator"
|
||||||
|
echo "-------------------------"
|
||||||
|
read -p "Telegram Token: " token
|
||||||
|
read -p "Telegram Chat_id: " chat_id
|
||||||
|
|
||||||
|
sed -e "s/\"max_open_trades\": 3,/\"max_open_trades\": $max_trades,/g" \
|
||||||
|
-e "s/\"stake_amount\": 0.05,/\"stake_amount\": $stake_amount,/g" \
|
||||||
|
-e "s/\"stake_currency\": \"BTC\",/\"stake_currency\": \"$stake_currency\",/g" \
|
||||||
|
-e "s/\"fiat_display_currency\": \"USD\",/\"fiat_display_currency\": \"$fiat_currency\",/g" \
|
||||||
|
-e "s/\"your_exchange_key\"/\"$api_key\"/g" \
|
||||||
|
-e "s/\"your_exchange_secret\"/\"$api_secret\"/g" \
|
||||||
|
-e "s/\"your_telegram_token\"/\"$token\"/g" \
|
||||||
|
-e "s/\"your_telegram_chat_id\"/\"$chat_id\"/g" \
|
||||||
|
-e "s/\"dry_run\": false,/\"dry_run\": true,/g" config.json.example > config.json
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function config () {
|
||||||
|
if [ -f config.json ]
|
||||||
|
then
|
||||||
|
read -p "A config file already exist, do you want to override it [Y/N]? "
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
config_generator
|
||||||
|
else
|
||||||
|
echo "Configuration of config.json ignored."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
config_generator
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Edit ./config.json to modify Pair and other configurations."
|
||||||
|
}
|
||||||
|
|
||||||
|
function install () {
|
||||||
|
echo "-------------------------"
|
||||||
|
echo "Install mandatory dependencies"
|
||||||
|
echo "-------------------------"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "$(uname -s)" == "Darwin" ]
|
||||||
|
then
|
||||||
|
echo "- You are on macOS"
|
||||||
|
install_macos
|
||||||
|
elif [ -x "$(command -v apt-get)" ]
|
||||||
|
then
|
||||||
|
echo "- You are on Debian/Ubuntu"
|
||||||
|
install_debian
|
||||||
|
else
|
||||||
|
echo "This script does not support your OS."
|
||||||
|
echo "If you have Python3.6, pip, virtualenv, ta-lib you can continue."
|
||||||
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
|
sleep 10
|
||||||
|
fi
|
||||||
|
reset
|
||||||
|
echo "
|
||||||
|
- Install complete.
|
||||||
|
"
|
||||||
|
config
|
||||||
|
echo "You can now use the bot by executing 'source .env/bin/activate; python3 freqtrade/main.py'."
|
||||||
|
}
|
||||||
|
|
||||||
|
function plot () {
|
||||||
|
echo "
|
||||||
|
-----------------------------------------
|
||||||
|
Install dependencies for Plotting scripts
|
||||||
|
-----------------------------------------
|
||||||
|
"
|
||||||
|
pip install plotly --upgrade
|
||||||
|
}
|
||||||
|
|
||||||
|
function help () {
|
||||||
|
echo "usage:"
|
||||||
|
echo " -i,--install Install freqtrade from scratch"
|
||||||
|
echo " -u,--update Command git pull to update."
|
||||||
|
echo " -r,--reset Hard reset your develop/master branch."
|
||||||
|
echo " -c,--config Easy config generator (Will override your existing file)."
|
||||||
|
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||||
|
}
|
||||||
|
|
||||||
|
case $* in
|
||||||
|
--install|-i)
|
||||||
|
install
|
||||||
|
;;
|
||||||
|
--config|-c)
|
||||||
|
config
|
||||||
|
;;
|
||||||
|
--update|-u)
|
||||||
|
update
|
||||||
|
;;
|
||||||
|
--reset|-r)
|
||||||
|
reset
|
||||||
|
;;
|
||||||
|
--plot|-p)
|
||||||
|
plot
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0
|
0
user_data/data/.gitkeep
Normal file
0
user_data/data/.gitkeep
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user