Merge e02f964e3a
into 2bb63ba33d
This commit is contained in:
commit
3dc429ab69
@ -1,10 +1,11 @@
|
|||||||
FROM python:3.6.5-slim-stretch
|
FROM python:3.6.6-slim-stretch
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
RUN apt-get update && apt-get -y install curl build-essential && 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 && \
|
||||||
|
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \
|
||||||
./configure && make && make install && \
|
./configure && make && make install && \
|
||||||
cd .. && rm -rf ta-lib
|
cd .. && rm -rf ta-lib
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
@ -15,7 +16,8 @@ WORKDIR /freqtrade
|
|||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt /freqtrade/
|
COPY requirements.txt /freqtrade/
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install numpy \
|
||||||
|
&& pip install -r requirements.txt
|
||||||
|
|
||||||
# Install and execute
|
# Install and execute
|
||||||
COPY . /freqtrade/
|
COPY . /freqtrade/
|
||||||
|
87
README.md
87
README.md
@ -4,13 +4,12 @@
|
|||||||
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
[](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
|
||||||
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
[](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
|
||||||
|
|
||||||
|
Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram.
|
||||||
Simple High frequency trading bot for crypto currencies designed to
|
|
||||||
support multi exchanges and be controlled via Telegram.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This software is for educational purposes only. Do not risk money which
|
This software is for educational purposes only. Do not risk money which
|
||||||
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
||||||
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||||
@ -23,18 +22,18 @@ We strongly recommend you to have coding and Python knowledge. Do not
|
|||||||
hesitate to read the source code and understand the mechanism of this bot.
|
hesitate to read the source code and understand the mechanism of this bot.
|
||||||
|
|
||||||
## Exchange marketplaces supported
|
## Exchange marketplaces supported
|
||||||
|
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [Binance](https://www.binance.com/)
|
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
|
||||||
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
|
||||||
Windows, macOS and Linux
|
- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux
|
||||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
- [x] **Persistence**: Persistence is achieved through sqlite
|
||||||
- [x] **Dry-run**: Run the bot without playing money.
|
- [x] **Dry-run**: Run the bot without playing money.
|
||||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||||
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
|
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
|
||||||
strategy parameters with real exchange data.
|
|
||||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
|
||||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
|
||||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||||
@ -43,38 +42,45 @@ strategy parameters with real exchange data.
|
|||||||
- [x] **Performance status report**: Provide a performance status of your current trades.
|
- [x] **Performance status report**: Provide a performance status of your current trades.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
|
||||||
- [Basic Usage](#basic-usage)
|
- [Basic Usage](#basic-usage)
|
||||||
- [Bot commands](#bot-commands)
|
- [Bot commands](#bot-commands)
|
||||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
- [Telegram RPC commands](#telegram-rpc-commands)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Help](#help--slack)
|
- [Help](#help--slack)
|
||||||
- [Bugs](#bugs--issues)
|
- [Bugs](#bugs--issues)
|
||||||
- [Feature Requests](#feature-requests)
|
- [Feature Requests](#feature-requests)
|
||||||
- [Pull Requests](#pull-requests)
|
- [Pull Requests](#pull-requests)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Min hardware required](#min-hardware-required)
|
- [Min hardware required](#min-hardware-required)
|
||||||
- [Software requirements](#software-requirements)
|
- [Software requirements](#software-requirements)
|
||||||
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:freqtrade/freqtrade.git
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
git checkout develop
|
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
|
git checkout develop
|
||||||
./setup.sh --install
|
./setup.sh --install
|
||||||
```
|
```
|
||||||
|
|
||||||
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
|
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
We invite you to read the bot documentation to ensure you understand how the bot is working.
|
||||||
|
|
||||||
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
|
||||||
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
|
||||||
@ -86,7 +92,6 @@ We invite you to read the bot documentation to ensure you understand how the bot
|
|||||||
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
### Bot commands
|
### Bot commands
|
||||||
@ -125,17 +130,15 @@ optional arguments:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Telegram RPC commands
|
### Telegram RPC commands
|
||||||
Telegram is not mandatory. However, this is a great way to control your
|
|
||||||
bot. More details on our
|
Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
||||||
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
|
|
||||||
|
|
||||||
- `/start`: Starts the trader
|
- `/start`: Starts the trader
|
||||||
- `/stop`: Stops the trader
|
- `/stop`: Stops the trader
|
||||||
- `/status [table]`: Lists all open trades
|
- `/status [table]`: Lists all open trades
|
||||||
- `/count`: Displays number of open trades
|
- `/count`: Displays number of open trades
|
||||||
- `/profit`: Lists cumulative profit from all finished trades
|
- `/profit`: Lists cumulative profit from all finished trades
|
||||||
- `/forcesell <trade_id>|all`: Instantly sells the given trade
|
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
(Ignoring `minimum_roi`).
|
|
||||||
- `/performance`: Show performance of each finished trade grouped by pair
|
- `/performance`: Show performance of each finished trade grouped by pair
|
||||||
- `/balance`: Show account balance per currency
|
- `/balance`: Show account balance per currency
|
||||||
- `/daily <n>`: Shows profit or loss per day, over the last n days
|
- `/daily <n>`: Shows profit or loss per day, over the last n days
|
||||||
@ -144,20 +147,30 @@ bot. More details on our
|
|||||||
|
|
||||||
|
|
||||||
## Development branches
|
## Development branches
|
||||||
The project is currently setup in two main branches:
|
|
||||||
- `develop` - This branch has often new features, but might also cause
|
|
||||||
breaking changes.
|
|
||||||
- `master` - This branch contains the latest stable release. The bot
|
|
||||||
'should' be stable on this branch, and is generally well tested.
|
|
||||||
|
|
||||||
|
The project is currently setup in two main branches:
|
||||||
|
|
||||||
|
- `develop` - This branch has often new features, but might also cause breaking changes.
|
||||||
|
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
|
||||||
|
- `feat/*` - These are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature.
|
||||||
|
|
||||||
|
|
||||||
|
## A note on Binance
|
||||||
|
|
||||||
|
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
|
||||||
|
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
### Help / Slack
|
### Help / Slack
|
||||||
|
|
||||||
For any questions not covered by the documentation or for further
|
For any questions not covered by the documentation or for further
|
||||||
information about the bot, we encourage you to join our slack channel.
|
information about the bot, we encourage you to join our slack channel.
|
||||||
|
|
||||||
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||||
|
|
||||||
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
|
|
||||||
If you discover a bug in the bot, please
|
If you discover a bug in the bot, please
|
||||||
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
|
||||||
first. If it hasn't been reported, please
|
first. If it hasn't been reported, please
|
||||||
@ -166,6 +179,7 @@ ensure you follow the template guide so that our team can assist you as
|
|||||||
quickly as possible.
|
quickly as possible.
|
||||||
|
|
||||||
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
|
||||||
|
|
||||||
Have you a great idea to improve the bot you want to share? Please,
|
Have you a great idea to improve the bot you want to share? Please,
|
||||||
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
|
||||||
If it hasn't been requested, please
|
If it hasn't been requested, please
|
||||||
@ -174,6 +188,7 @@ and ensure you follow the template guide so that it does not get lost
|
|||||||
in the bug reports.
|
in the bug reports.
|
||||||
|
|
||||||
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||||
Please read our
|
Please read our
|
||||||
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
@ -181,16 +196,18 @@ to understand the requirements before sending your pull-requests.
|
|||||||
|
|
||||||
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
|
||||||
|
|
||||||
**Important:** Always create your PR against the `develop` branch, not
|
**Important:** Always create your PR against the `develop` branch, not `master`.
|
||||||
`master`.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Min hardware required
|
### Min hardware required
|
||||||
|
|
||||||
To run this bot we recommend you a cloud instance with a minimum of:
|
To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
* Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
|
|
||||||
|
- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
|
||||||
|
|
||||||
### Software requirements
|
### Software requirements
|
||||||
|
|
||||||
- [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/)
|
||||||
- [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)
|
||||||
|
@ -5,7 +5,11 @@
|
|||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"ticker_interval" : "5m",
|
"ticker_interval" : "5m",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"unfilledtimeout": 600,
|
"trailing_stop": false,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30
|
||||||
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
@ -13,6 +17,7 @@
|
|||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
|
"ccxt_rate_limit": true,
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
"ETH/BTC",
|
||||||
"LTC/BTC",
|
"LTC/BTC",
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
"fiat_display_currency": "USD",
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"ticker_interval": "5m",
|
"ticker_interval": "5m",
|
||||||
|
"trailing_stop": false,
|
||||||
|
"trailing_stop_positive": 0.005,
|
||||||
|
"trailing_stop_positive_offset": 0.0051,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
@ -12,7 +15,10 @@
|
|||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30
|
||||||
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
@ -20,6 +26,7 @@
|
|||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "your_exchange_key",
|
"key": "your_exchange_key",
|
||||||
"secret": "your_exchange_secret",
|
"secret": "your_exchange_secret",
|
||||||
|
"ccxt_rate_limit": true,
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"ETH/BTC",
|
"ETH/BTC",
|
||||||
"LTC/BTC",
|
"LTC/BTC",
|
||||||
|
@ -29,25 +29,25 @@ The backtesting is very easy with freqtrade.
|
|||||||
#### With 5 min tickers (Per default)
|
#### With 5 min tickers (Per default)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation
|
python3 ./freqtrade/main.py backtesting
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With 1 min tickers
|
#### With 1 min tickers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
|
python3 ./freqtrade/main.py backtesting --ticker-interval 1m
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Update cached pairs with the latest data
|
#### Update cached pairs with the latest data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
|
python3 ./freqtrade/main.py backtesting --refresh-pairs-cached
|
||||||
```
|
```
|
||||||
|
|
||||||
#### With live data (do not alter your testdata files)
|
#### With live data (do not alter your testdata files)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
python3 ./freqtrade/main.py backtesting --live
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using a different on-disk ticker-data source
|
#### Using a different on-disk ticker-data source
|
||||||
@ -70,6 +70,36 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
|
|||||||
python3 ./freqtrade/main.py backtesting --export trades
|
python3 ./freqtrade/main.py backtesting --export trades
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
filename=Path('user_data/backtest_data/backtest-result.json')
|
||||||
|
|
||||||
|
with filename.open() as file:
|
||||||
|
data = json.load(file)
|
||||||
|
|
||||||
|
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||||
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
df = pd.DataFrame(data, columns=columns)
|
||||||
|
|
||||||
|
df['opents'] = pd.to_datetime(df['opents'],
|
||||||
|
unit='s',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True
|
||||||
|
)
|
||||||
|
df['closets'] = pd.to_datetime(df['closets'],
|
||||||
|
unit='s',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.
|
||||||
|
|
||||||
#### Exporting trades to file specifying a custom filename
|
#### Exporting trades to file specifying a custom filename
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -121,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance
|
|||||||
Then run:
|
Then run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/download_backtest_data --exchange binance
|
python scripts/download_backtest_data.py --exchange binance
|
||||||
```
|
```
|
||||||
|
|
||||||
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
This will download ticker data for all the currency pairs you defined in `pairs.json`.
|
||||||
@ -208,6 +238,31 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
|
|||||||
profit. Hence, keep in mind that your performance is a mix of your
|
profit. Hence, keep in mind that your performance is a mix of your
|
||||||
strategies, your configuration, and the crypto-currency you have set up.
|
strategies, your configuration, and the crypto-currency you have set up.
|
||||||
|
|
||||||
|
## Backtesting multiple strategies
|
||||||
|
|
||||||
|
To backtest multiple strategies, a list of Strategies can be provided.
|
||||||
|
|
||||||
|
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
|
||||||
|
strategies you'd like to compare, this should give a nice runtime boost.
|
||||||
|
|
||||||
|
All listed Strategies need to be in the same folder.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
|
||||||
|
```
|
||||||
|
|
||||||
|
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
|
||||||
|
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
|
||||||
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
|
||||||
|
|
||||||
|
```
|
||||||
|
=================================================== Strategy Summary ====================================================
|
||||||
|
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss |
|
||||||
|
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:|
|
||||||
|
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 |
|
||||||
|
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 |
|
||||||
|
```
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
||||||
Great, your strategy is profitable. What if the bot can give your the
|
Great, your strategy is profitable. What if the bot can give your the
|
||||||
|
@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
|
|||||||
- Sell strategy rules
|
- Sell strategy rules
|
||||||
- Minimal ROI recommended
|
- Minimal ROI recommended
|
||||||
- Stoploss recommended
|
- Stoploss recommended
|
||||||
- Hyperopt parameter
|
|
||||||
|
|
||||||
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
|
||||||
You can test it with the parameter: `--strategy TestStrategy`
|
You can test it with the parameter: `--strategy TestStrategy`
|
||||||
@ -61,22 +60,22 @@ file as reference.**
|
|||||||
|
|
||||||
### Buy strategy
|
### Buy strategy
|
||||||
|
|
||||||
Edit the method `populate_buy_trend()` into your strategy file to
|
Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
|
||||||
update your buy strategy.
|
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> 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 populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 30) &
|
(dataframe['adx'] > 30) &
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] > dataframe['tema'].shift(1))
|
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|||||||
### Sell strategy
|
### Sell strategy
|
||||||
|
|
||||||
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
|
||||||
|
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
|
||||||
|
|
||||||
Sample from `user_data/strategies/test_strategy.py`:
|
Sample from `user_data/strategies/test_strategy.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> 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 populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 70) &
|
(dataframe['adx'] > 70) &
|
||||||
(dataframe['tema'] > dataframe['blower']) &
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
(dataframe['tema'] < dataframe['tema'].shift(1))
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
),
|
),
|
||||||
'sell'] = 1
|
'sell'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Add more Indicator
|
## Add more Indicators
|
||||||
|
|
||||||
As you have seen, buy and sell strategies need indicators. You can add
|
As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
|
||||||
more indicators by extending the list contained in
|
|
||||||
the method `populate_indicators()` from your strategy file.
|
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
|
||||||
|
|
||||||
Sample:
|
Sample:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
|
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.
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Metadata dict
|
||||||
|
|
||||||
|
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
|
||||||
|
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
|
||||||
|
|
||||||
### Want more indicator examples
|
### Want more indicator examples
|
||||||
|
|
||||||
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
# Bot usage
|
# Bot usage
|
||||||
This page explains the difference parameters of the bot and how to run
|
|
||||||
it.
|
This page explains the difference parameters of the bot and how to run it.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Bot commands](#bot-commands)
|
- [Bot commands](#bot-commands)
|
||||||
- [Backtesting commands](#backtesting-commands)
|
- [Backtesting commands](#backtesting-commands)
|
||||||
- [Hyperopt commands](#hyperopt-commands)
|
- [Hyperopt commands](#hyperopt-commands)
|
||||||
|
|
||||||
## Bot commands
|
## Bot commands
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
|
||||||
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
[--strategy-path PATH] [--dynamic-whitelist [INT]]
|
||||||
@ -41,6 +43,7 @@ optional arguments:
|
|||||||
```
|
```
|
||||||
|
|
||||||
### How to use a different config file?
|
### How to use a different config file?
|
||||||
|
|
||||||
The bot allows you to select which config file you want to use. Per
|
The bot allows you to select which config file you want to use. Per
|
||||||
default, the bot will load the file `./config.json`
|
default, the bot will load the file `./config.json`
|
||||||
|
|
||||||
@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json
|
|||||||
```
|
```
|
||||||
|
|
||||||
### How to use --strategy?
|
### How to use --strategy?
|
||||||
|
|
||||||
This parameter will allow you to load your custom strategy class.
|
This parameter will allow you to load your custom strategy class.
|
||||||
Per default without `--strategy` or `-s` the bot will load the
|
Per default without `--strategy` or `-s` the bot will load the
|
||||||
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
|
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
|
||||||
@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this
|
|||||||
**Example:**
|
**Example:**
|
||||||
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
|
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
|
||||||
a strategy class called `AwesomeStrategy` to load it:
|
a strategy class called `AwesomeStrategy` to load it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
python3 ./freqtrade/main.py --strategy AwesomeStrategy
|
||||||
```
|
```
|
||||||
@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code).
|
|||||||
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
|
||||||
### How to use --strategy-path?
|
### How to use --strategy-path?
|
||||||
|
|
||||||
This parameter allows you to add an additional strategy lookup path, which gets
|
This parameter allows you to add an additional strategy lookup path, which gets
|
||||||
checked before the default locations (The passed path must be a folder!):
|
checked before the default locations (The passed path must be a folder!):
|
||||||
```bash
|
```bash
|
||||||
@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### How to install a strategy?
|
#### How to install a strategy?
|
||||||
|
|
||||||
This is very simple. Copy paste your strategy file into the folder
|
This is very simple. Copy paste your strategy file into the folder
|
||||||
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
|
`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.
|
||||||
|
|
||||||
**By Default**
|
**By Default**
|
||||||
Get the 20 currencies based on BaseVolume.
|
Get the 20 currencies based on BaseVolume.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py --dynamic-whitelist
|
python3 ./freqtrade/main.py --dynamic-whitelist
|
||||||
```
|
```
|
||||||
|
|
||||||
**Customize the number of currencies to retrieve**
|
**Customize the number of currencies to retrieve**
|
||||||
Get the 30 currencies based on BaseVolume.
|
Get the 30 currencies based on BaseVolume.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 ./freqtrade/main.py --dynamic-whitelist 30
|
python3 ./freqtrade/main.py --dynamic-whitelist 30
|
||||||
```
|
```
|
||||||
@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default
|
|||||||
value (20).
|
value (20).
|
||||||
|
|
||||||
### How to use --db-url?
|
### How to use --db-url?
|
||||||
|
|
||||||
When you run the bot in Dry-run mode, per default no transactions are
|
When you run the bot in Dry-run mode, per default no transactions are
|
||||||
stored in a database. If you want to store your bot actions in a DB
|
stored in a database. If you want to store your bot actions in a DB
|
||||||
using `--db-url`. This can also be used to specify a custom database
|
using `--db-url`. This can also be used to specify a custom database
|
||||||
@ -111,24 +122,27 @@ in production mode. Example command:
|
|||||||
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Backtesting commands
|
## Backtesting commands
|
||||||
|
|
||||||
Backtesting also uses the config specified via `-c/--config`.
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
|
||||||
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
|
[--timerange TIMERANGE] [-l] [-r]
|
||||||
[--export-filename EXPORTFILENAME]
|
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
|
||||||
|
[--export EXPORT] [--export-filename PATH]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
--realistic-simulation
|
--eps, --enable-position-stacking
|
||||||
uses max_open_trades from config to simulate real
|
Allow buying the same pair multiple times (position
|
||||||
world limitations
|
stacking)
|
||||||
|
--dmmp, --disable-max-market-positions
|
||||||
|
Disable applying `max_open_trades` during backtest
|
||||||
|
(same as setting `max_open_trades` to a very high
|
||||||
|
number)
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
specify what timerange of data to use.
|
specify what timerange of data to use.
|
||||||
-l, --live using live data
|
-l, --live using live data
|
||||||
@ -136,16 +150,26 @@ optional arguments:
|
|||||||
refresh the pairs files in tests/testdata with the
|
refresh the pairs files in tests/testdata with the
|
||||||
latest data from the exchange. Use it if you want to
|
latest data from the exchange. Use it if you want to
|
||||||
run your backtesting with up-to-date data.
|
run your backtesting with up-to-date data.
|
||||||
|
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
|
||||||
|
Provide a commaseparated list of strategies to
|
||||||
|
backtest Please note that ticker-interval needs to be
|
||||||
|
set either in config or via command line. When using
|
||||||
|
this together with --export trades, the strategy-name
|
||||||
|
is injected into the filename (so backtest-data.json
|
||||||
|
becomes backtest-data-DefaultStrategy.json
|
||||||
--export EXPORT export backtest results, argument are: trades Example
|
--export EXPORT export backtest results, argument are: trades Example
|
||||||
--export=trades
|
--export=trades
|
||||||
--export-filename EXPORTFILENAME
|
--export-filename PATH
|
||||||
Save backtest results to this filename requires
|
Save backtest results to this filename requires
|
||||||
--export to be set as well Example --export-
|
--export to be set as well Example --export-
|
||||||
filename=backtest_today.json (default: backtest-
|
filename=user_data/backtest_data/backtest_today.json
|
||||||
result.json
|
(default: user_data/backtest_data/backtest-
|
||||||
|
result.json)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### How to use --refresh-pairs-cached parameter?
|
### How to use --refresh-pairs-cached parameter?
|
||||||
|
|
||||||
The first time your run Backtesting, it will take the pairs you have
|
The first time your run Backtesting, it will take the pairs you have
|
||||||
set in your config file and download data from Bittrex.
|
set in your config file and download data from Bittrex.
|
||||||
|
|
||||||
@ -157,36 +181,42 @@ to come back to the previous version.**
|
|||||||
To test your strategy with latest data, we recommend continuing using
|
To test your strategy with latest data, we recommend continuing using
|
||||||
the parameter `-l` or `--live`.
|
the parameter `-l` or `--live`.
|
||||||
|
|
||||||
|
|
||||||
## Hyperopt commands
|
## Hyperopt commands
|
||||||
|
|
||||||
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
To optimize your strategy, you can use hyperopt parameter hyperoptimization
|
||||||
to find optimal parameter values for your stategy.
|
to find optimal parameter values for your stategy.
|
||||||
|
|
||||||
```
|
```
|
||||||
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
|
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
|
||||||
[--timerange TIMERANGE] [-e INT]
|
[--timerange TIMERANGE] [-e INT]
|
||||||
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
|
||||||
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
specify ticker interval (1m, 5m, 30m, 1h, 1d)
|
||||||
--realistic-simulation
|
--eps, --enable-position-stacking
|
||||||
uses max_open_trades from config to simulate real
|
Allow buying the same pair multiple times (position
|
||||||
world limitations
|
stacking)
|
||||||
--timerange TIMERANGE specify what timerange of data to use.
|
--dmmp, --disable-max-market-positions
|
||||||
|
Disable applying `max_open_trades` during backtest
|
||||||
|
(same as setting `max_open_trades` to a very high
|
||||||
|
number)
|
||||||
|
--timerange TIMERANGE
|
||||||
|
specify what timerange of data to use.
|
||||||
-e INT, --epochs INT specify number of epochs (default: 100)
|
-e INT, --epochs INT specify number of epochs (default: 100)
|
||||||
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
|
||||||
Specify which parameters to hyperopt. Space separate
|
Specify which parameters to hyperopt. Space separate
|
||||||
list. Default: all
|
list. Default: all
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## A parameter missing in the configuration?
|
## A parameter missing in the configuration?
|
||||||
|
|
||||||
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
|
||||||
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
The optimal strategy of the bot will change with time depending of the
|
|
||||||
market trends. The next step is to
|
The optimal strategy of the bot will change with time depending of the market trends. The next step is to
|
||||||
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
# Configure the bot
|
# Configure the bot
|
||||||
|
|
||||||
This page explains how to configure your `config.json` file.
|
This page explains how to configure your `config.json` file.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Bot commands](#bot-commands)
|
- [Bot commands](#bot-commands)
|
||||||
- [Backtesting commands](#backtesting-commands)
|
- [Backtesting commands](#backtesting-commands)
|
||||||
- [Hyperopt commands](#hyperopt-commands)
|
- [Hyperopt commands](#hyperopt-commands)
|
||||||
|
|
||||||
## Setup config.json
|
## Setup config.json
|
||||||
|
|
||||||
We recommend to copy and use the `config.json.example` as a template
|
We recommend to copy and use the `config.json.example` as a template
|
||||||
for your bot configuration.
|
for your bot configuration.
|
||||||
|
|
||||||
@ -22,7 +25,11 @@ The table below will list all configuration parameters.
|
|||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||||
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
|
||||||
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
|
| `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
|
||||||
|
| `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached.
|
||||||
|
| `trailing_stoploss_positve_offset` | 0 | No | Offset on when to apply `trailing_stoploss_positive`. Percentage value which should be positive.
|
||||||
|
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
|
||||||
|
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
|
||||||
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
| `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. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
@ -35,16 +42,21 @@ The table below will list all configuration parameters.
|
|||||||
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
|
||||||
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
|
||||||
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
|
||||||
|
| `webhook.enabled` | false | No | Enable useage of Webhook notifications
|
||||||
|
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
|
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
|
||||||
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
|
||||||
| `initial_state` | running | No | Defines the initial application state. More information below.
|
| `initial_state` | running | No | Defines the initial application state. More information below.
|
||||||
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
|
||||||
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
|
||||||
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
|
||||||
|
|
||||||
The definition of each config parameters is in
|
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
||||||
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
|
|
||||||
|
|
||||||
### Understand stake_amount
|
### Understand stake_amount
|
||||||
|
|
||||||
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
`stake_amount` is an amount of crypto-currency your bot will use for each trade.
|
||||||
The minimal value is 0.0005. If there is not enough crypto-currency in
|
The minimal value is 0.0005. If there is not enough crypto-currency in
|
||||||
the account an exception is generated.
|
the account an exception is generated.
|
||||||
@ -52,9 +64,11 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set
|
|||||||
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
|
||||||
|
|
||||||
### Understand minimal_roi
|
### Understand minimal_roi
|
||||||
|
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
|
|
||||||
```
|
```
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
"40": 0.0, # Sell after 40 minutes if the profit is not negative
|
||||||
@ -69,6 +83,7 @@ value. This parameter is optional. If you use it, it will take over the
|
|||||||
`minimal_roi` value from the strategy file.
|
`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.
|
||||||
@ -77,82 +92,100 @@ Most of the strategy files already include the optimal `stoploss`
|
|||||||
value. This parameter is optional. If you use it, it will take over the
|
value. This parameter is optional. If you use it, it will take over the
|
||||||
`stoploss` value from the strategy file.
|
`stoploss` value from the strategy file.
|
||||||
|
|
||||||
|
### Understand trailing stoploss
|
||||||
|
|
||||||
|
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
|
||||||
|
|
||||||
### 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`)
|
||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
|
|
||||||
### Understand process_throttle_secs
|
### Understand process_throttle_secs
|
||||||
|
|
||||||
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
|
||||||
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
|
||||||
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
|
||||||
the static list of pairs) if we should buy.
|
the static list of pairs) if we should buy.
|
||||||
|
|
||||||
### Understand ask_last_balance
|
### Understand ask_last_balance
|
||||||
|
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||||
use the `last` price and values between those interpolate between ask and last
|
use the `last` price and values between those interpolate between ask and last
|
||||||
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||||
end up paying more then would probably have been necessary.
|
end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
### What values for exchange.name?
|
### What values for exchange.name?
|
||||||
|
|
||||||
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
|
||||||
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
exchange markets and trading APIs. The complete up-to-date list can be found in the
|
||||||
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
|
||||||
with only Bittrex and Binance.
|
with only Bittrex and Binance.
|
||||||
|
|
||||||
The bot was tested with the following exchanges:
|
The bot was tested with the following exchanges:
|
||||||
|
|
||||||
- [Bittrex](https://bittrex.com/): "bittrex"
|
- [Bittrex](https://bittrex.com/): "bittrex"
|
||||||
- [Binance](https://www.binance.com/): "binance"
|
- [Binance](https://www.binance.com/): "binance"
|
||||||
|
|
||||||
Feel free to test other exchanges and submit your PR to improve the bot.
|
Feel free to test other exchanges and submit your PR to improve the bot.
|
||||||
|
|
||||||
### What values for fiat_display_currency?
|
### What values for fiat_display_currency?
|
||||||
|
|
||||||
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
|
||||||
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
|
||||||
In addition to central bank currencies, a range of cryto currencies are supported.
|
In addition to central bank currencies, a range of cryto currencies are supported.
|
||||||
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
|
||||||
|
|
||||||
## Switch to dry-run mode
|
## Switch to dry-run mode
|
||||||
|
|
||||||
We recommend starting the bot in dry-run mode to see how your bot will
|
We recommend starting the bot in dry-run mode to see how your bot will
|
||||||
behave and how is the performance of your strategy. In Dry-run mode the
|
behave and how is the performance of your strategy. In Dry-run mode the
|
||||||
bot does not engage your money. It only runs a live simulation without
|
bot does not engage your money. It only runs a live simulation without
|
||||||
creating trades.
|
creating trades.
|
||||||
|
|
||||||
### To switch your bot in Dry-run mode:
|
### To switch your bot in Dry-run mode:
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
2. Switch dry-run to true and specify db_url for a persistent db
|
2. Switch dry-run to true and specify db_url for a persistent db
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": true,
|
"dry_run": true,
|
||||||
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
"db_url": "sqlite///tradesv3.dryrun.sqlite",
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Remove your Exchange API key (change them by fake api credentials)
|
3. Remove your Exchange API key (change them by fake api credentials)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Once you will be happy with your bot performance, you can switch it to
|
Once you will be happy with your bot performance, you can switch it to
|
||||||
production mode.
|
production mode.
|
||||||
|
|
||||||
## Switch to production mode
|
## Switch to production mode
|
||||||
|
|
||||||
In production mode, the bot will engage your money. Be careful a wrong
|
In production mode, the bot will engage your money. Be careful a wrong
|
||||||
strategy can lose all your money. Be aware of what you are doing when
|
strategy can lose all your money. Be aware of what you are doing when
|
||||||
you run it in production mode.
|
you run it in production mode.
|
||||||
|
|
||||||
### To switch your bot in production mode:
|
### To switch your bot in production mode:
|
||||||
|
|
||||||
1. Edit your `config.json` file
|
1. Edit your `config.json` file
|
||||||
|
|
||||||
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
2. Switch dry-run to false and don't forget to adapt your database URL if set
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Insert your Exchange API key (change them by fake api keys)
|
3. Insert your Exchange API key (change them by fake api keys)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
@ -160,10 +193,37 @@ you run it in production mode.
|
|||||||
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
If you have not your Bittrex API key yet,
|
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
||||||
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
|
|
||||||
|
|
||||||
|
### Embedding Strategies
|
||||||
|
|
||||||
|
FreqTrade provides you with with an easy way to embed the strategy into your configuration file.
|
||||||
|
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
|
||||||
|
in your chosen config file.
|
||||||
|
|
||||||
|
##### Encoding a string as BASE64
|
||||||
|
|
||||||
|
This is a quick example, how to generate the BASE64 string in python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from base64 import urlsafe_b64encode
|
||||||
|
|
||||||
|
with open(file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
content = urlsafe_b64encode(content.encode('utf-8'))
|
||||||
|
```
|
||||||
|
|
||||||
|
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
|
||||||
|
|
||||||
|
```json
|
||||||
|
"strategy": "NameOfStrategy:BASE64String"
|
||||||
|
```
|
||||||
|
|
||||||
|
Please ensure that 'NameOfStrategy' is identical to the strategy name!
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
Now you have configured your config.json, the next step is to
|
|
||||||
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).
|
||||||
|
300
docs/hyperopt.md
300
docs/hyperopt.md
@ -1,155 +1,114 @@
|
|||||||
# Hyperopt
|
# Hyperopt
|
||||||
This page explains how to tune your strategy by finding the optimal
|
This page explains how to tune your strategy by finding the optimal
|
||||||
parameters with Hyperopt.
|
parameters, a process called hyperparameter optimization. The bot uses several
|
||||||
|
algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||||
|
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||||
|
and still take a long time.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
- [Prepare your Hyperopt](#prepare-hyperopt)
|
||||||
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers)
|
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
||||||
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file)
|
- [Solving a Mystery](#solving-a-mystery)
|
||||||
- [Advanced Hyperopt notions](#advanced-notions)
|
- [Adding New Indicators](#adding-new-indicators)
|
||||||
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
|
|
||||||
- [Execute Hyperopt](#execute-hyperopt)
|
- [Execute Hyperopt](#execute-hyperopt)
|
||||||
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
- [Understand the hyperopts result](#understand-the-backtesting-result)
|
||||||
|
|
||||||
## Prepare Hyperopt
|
## Prepare Hyperopting
|
||||||
Before we start digging in Hyperopt, we recommend you to take a look at
|
We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
|
||||||
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
|
|
||||||
|
|
||||||
### 1. Configure your Guards and Triggers
|
### Configure your Guards and Triggers
|
||||||
There are two places you need to change in your strategy file to add a
|
There are two places you need to change to add a new buy strategy for testing:
|
||||||
new buy strategy for testing:
|
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
|
||||||
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
|
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
|
||||||
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
|
and the associated methods `indicator_space`, `roi_space`, `stoploss_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`.
|
1. Guards are conditions like "never buy if ADX < 10", or "never buy if
|
||||||
1. Guards are conditions like "never buy if ADX < 10", or never buy if
|
current price is over EMA10".
|
||||||
current price is over EMA10.
|
|
||||||
2. Triggers are ones that actually trigger buy in specific moment, like
|
2. Triggers are ones that actually trigger buy in specific moment, like
|
||||||
"buy when EMA5 crosses over EMA10" or buy when close price touches lower
|
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
|
||||||
bollinger band.
|
bollinger band".
|
||||||
|
|
||||||
HyperOpt will, for each eval round, pick just ONE trigger, and possibly
|
Hyperoptimization will, for each eval round, pick one trigger and possibly
|
||||||
multiple guards. So that the constructed strategy will be something like
|
multiple guards. The constructed strategy will be something like
|
||||||
"*buy exactly when close price touches lower bollinger band, BUT only if
|
"*buy exactly when close price touches lower bollinger band, BUT only if
|
||||||
ADX > 10*".
|
ADX > 10*".
|
||||||
|
|
||||||
|
If you have updated the buy strategy, ie. changed the contents of
|
||||||
If you have updated the buy strategy, means change the content of
|
|
||||||
`populate_buy_trend()` method 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 use.
|
||||||
|
|
||||||
As for an example if your `populate_buy_trend()` method is:
|
## Solving a Mystery
|
||||||
```python
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(dataframe['rsi'] < 35) &
|
|
||||||
(dataframe['adx'] > 65),
|
|
||||||
'buy'] = 1
|
|
||||||
|
|
||||||
return dataframe
|
Let's say you are curious: should you use MACD crossings or lower Bollinger
|
||||||
```
|
Bands to trigger your buys. And you also wonder should you use RSI or ADX to
|
||||||
|
help with those buy decisions. If you decide to use RSI or ADX, which values
|
||||||
|
should I use for them? So let's use hyperparameter optimization to solve this
|
||||||
|
mystery.
|
||||||
|
|
||||||
Your hyperopt file must contain `guards` to find the right value for
|
We will start by defining a search space:
|
||||||
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
|
|
||||||
means you will need to enable/disable triggers.
|
|
||||||
|
|
||||||
In our case the `SPACE` and `populate_buy_trend` in your strategy file
|
|
||||||
will look like:
|
|
||||||
```python
|
|
||||||
space = {
|
|
||||||
'rsi': hp.choice('rsi', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
|
||||||
]),
|
|
||||||
'adx': hp.choice('adx', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
|
|
||||||
]),
|
|
||||||
'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'},
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
|
||||||
conditions = []
|
|
||||||
# GUARDS AND TRENDS
|
|
||||||
if params['adx']['enabled']:
|
|
||||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
|
||||||
if params['rsi']['enabled']:
|
|
||||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
|
||||||
|
|
||||||
# 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'])),
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 2. Update the hyperopt config file
|
|
||||||
Hyperopt is using a dedicated config file. Currently hyperopt
|
|
||||||
cannot use your config file. It is also made on purpose to allow you
|
|
||||||
testing your strategy with different configurations.
|
|
||||||
|
|
||||||
The Hyperopt configuration is located in
|
|
||||||
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
|
|
||||||
|
|
||||||
|
|
||||||
## Advanced notions
|
|
||||||
### Understand the Guards and Triggers
|
|
||||||
When you need to add the new guards and triggers to be hyperopt
|
|
||||||
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
|
|
||||||
|
|
||||||
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
|
|
||||||
|
|
||||||
If it's a guard, you will add a line like this:
|
|
||||||
```
|
|
||||||
'rsi': hp.choice('rsi', [
|
|
||||||
{'enabled': False},
|
|
||||||
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
|
|
||||||
]),
|
|
||||||
```
|
|
||||||
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*".
|
|
||||||
|
|
||||||
So, the part of the strategy builder using the above setting looks like
|
|
||||||
this:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
if params['rsi']['enabled']:
|
def indicator_space() -> List[Dimension]:
|
||||||
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
"""
|
||||||
|
Define your Hyperopt space for searching strategy parameters
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
Integer(20, 40, name='adx-value'),
|
||||||
|
Integer(20, 40, name='rsi-value'),
|
||||||
|
Categorical([True, False], name='adx-enabled'),
|
||||||
|
Categorical([True, False], name='rsi-enabled'),
|
||||||
|
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
It checks if Hyperopt wants the RSI guard to be enabled for this
|
Above definition says: I have five parameters I want you to randomly combine
|
||||||
round `params['rsi']['enabled']` and if it is, then it will add a
|
to find the best combination. Two of them are integer values (`adx-value`
|
||||||
condition that says RSI must be smaller than the value hyperopt picked
|
and `rsi-value`) and I want you test in the range of values 20 to 40.
|
||||||
for this evaluation, which is given in the `params['rsi']['value']`.
|
Then we have three category variables. First two are either `True` or `False`.
|
||||||
|
We use these to either enable or disable the ADX and RSI guards. The last
|
||||||
|
one we call `trigger` and use it to decide which buy trigger we want to use.
|
||||||
|
|
||||||
That's it. Now you can add new parts of strategies to Hyperopt and it
|
So let's write the buy strategy using these values:
|
||||||
will try all the combinations with all different values in the search
|
|
||||||
for best working algo.
|
|
||||||
|
|
||||||
|
```
|
||||||
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if 'adx-enabled' in params and params['adx-enabled']:
|
||||||
|
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||||
|
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] < params['rsi-value'])
|
||||||
|
|
||||||
### Add a new Indicators
|
# TRIGGERS
|
||||||
If you want to test an indicator that isn't used by the bot currently,
|
if params['trigger'] == 'bb_lower':
|
||||||
you need to add it to the `populate_indicators()` method in `hyperopt.py`.
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
|
dataframe['macd'], dataframe['macdsignal']
|
||||||
|
))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
return populate_buy_trend
|
||||||
|
```
|
||||||
|
|
||||||
|
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
|
||||||
|
with different value combinations. It will then use the given historical data and make
|
||||||
|
buys based on the buy signals generated with the above function and based on the results
|
||||||
|
it will end with telling you which paramter combination produced the best profits.
|
||||||
|
|
||||||
|
The search for best parameters starts with a few random combinations and then uses a
|
||||||
|
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
|
||||||
|
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
|
||||||
|
|
||||||
|
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||||
|
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||||
|
add it to the `populate_indicators()` method in `hyperopt.py`.
|
||||||
|
|
||||||
## Execute Hyperopt
|
## Execute Hyperopt
|
||||||
Once you have updated your hyperopt configuration you can run it.
|
Once you have updated your hyperopt configuration you can run it.
|
||||||
@ -164,12 +123,12 @@ python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
|
|||||||
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
The `-e` flag will set how many evaluations hyperopt will do. We recommend
|
||||||
running at least several thousand evaluations.
|
running at least several thousand evaluations.
|
||||||
|
|
||||||
### Execute hyperopt with different ticker-data source
|
### Execute Hyperopt with Different Ticker-Data Source
|
||||||
If you would like to hyperopt parameters using an alternate ticker data that
|
If you would like to hyperopt parameters using an alternate ticker data that
|
||||||
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
you have on-disk, use the `--datadir PATH` option. Default hyperopt will
|
||||||
use data from directory `user_data/data`.
|
use data from directory `user_data/data`.
|
||||||
|
|
||||||
### Running hyperopt with smaller testset
|
### Running Hyperopt with Smaller Testset
|
||||||
Use the `--timeperiod` argument to change how much of the testset
|
Use the `--timeperiod` argument to change how much of the testset
|
||||||
you want to use. The last N ticks/timeframes will be used.
|
you want to use. The last N ticks/timeframes will be used.
|
||||||
Example:
|
Example:
|
||||||
@ -178,7 +137,7 @@ Example:
|
|||||||
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
python3 ./freqtrade/main.py hyperopt --timeperiod -200
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running hyperopt with smaller search space
|
### Running Hyperopt with Smaller Search Space
|
||||||
Use the `--spaces` argument to limit the search space used by hyperopt.
|
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||||
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
Letting Hyperopt optimize everything is a huuuuge search space. Often it
|
||||||
might make more sense to start by just searching for initial buy algorithm.
|
might make more sense to start by just searching for initial buy algorithm.
|
||||||
@ -193,87 +152,44 @@ Legal values are:
|
|||||||
- `stoploss`: search for the best stoploss value
|
- `stoploss`: search for the best stoploss value
|
||||||
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
- space-separated list of any of the above values for example `--spaces roi stoploss`
|
||||||
|
|
||||||
## Understand the hyperopts result
|
## Understand the Hyperopts Result
|
||||||
Once Hyperopt is completed you can use the result to adding new buy
|
Once Hyperopt is completed you can use the result to create a new strategy.
|
||||||
signal. Given following result from hyperopt:
|
Given the following result from hyperopt:
|
||||||
```
|
|
||||||
Best parameters:
|
|
||||||
{
|
|
||||||
"adx": {
|
|
||||||
"enabled": true,
|
|
||||||
"value": 15.0
|
|
||||||
},
|
|
||||||
"fastd": {
|
|
||||||
"enabled": true,
|
|
||||||
"value": 40.0
|
|
||||||
},
|
|
||||||
"green_candle": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"mfi": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"over_sar": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"rsi": {
|
|
||||||
"enabled": true,
|
|
||||||
"value": 37.0
|
|
||||||
},
|
|
||||||
"trigger": {
|
|
||||||
"type": "lower_bb"
|
|
||||||
},
|
|
||||||
"uptrend_long_ema": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"uptrend_short_ema": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"uptrend_sma": {
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Best Result:
|
```
|
||||||
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins.
|
Best result:
|
||||||
|
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
|
||||||
|
with values:
|
||||||
|
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
|
||||||
```
|
```
|
||||||
|
|
||||||
You should understand this result like:
|
You should understand this result like:
|
||||||
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`)
|
- The buy trigger that worked best was `bb_lower`.
|
||||||
and the best value is `15.0` (`"value": 15.0,`)
|
- You should not use ADX because `adx-enabled: False`)
|
||||||
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
|
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
|
||||||
true`) and the best value is `40.0` (`"value": 40.0,`)
|
|
||||||
- You should **consider** to enable the guard "green_candle"
|
|
||||||
(`"green_candle"` is `"enabled": true`) but this guards as no
|
|
||||||
customizable value.
|
|
||||||
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
|
|
||||||
- and so on...
|
|
||||||
|
|
||||||
You have to look inside your strategy file into `buy_strategy_generator()`
|
You have to look inside your strategy file into `buy_strategy_generator()`
|
||||||
method, what those values match to.
|
method, what those values match to.
|
||||||
|
|
||||||
So for example you had `adx:` with the `value: 15.0` so we would look
|
So for example you had `rsi-value: 29.0` so we would look
|
||||||
at `adx`-block, that translates to the following code block:
|
at `rsi`-block, that translates to the following code block:
|
||||||
```
|
```
|
||||||
(dataframe['adx'] > 15.0)
|
(dataframe['rsi'] < 29.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
Translating your whole hyperopt result to as the new buy-signal
|
Translating your whole hyperopt result as the new buy-signal
|
||||||
would be the following:
|
would then look like:
|
||||||
```
|
```
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['adx'] > 15.0) & # adx-value
|
(dataframe['rsi'] < 29.0) & # rsi-value
|
||||||
(dataframe['fastd'] < 40.0) & # fastd-value
|
dataframe['close'] < dataframe['bb_lowerband'] # trigger
|
||||||
(dataframe['close'] > dataframe['open']) & # green_candle
|
|
||||||
(dataframe['rsi'] < 37.0) & # rsi-value
|
|
||||||
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
|
|
||||||
),
|
),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
return dataframe
|
return dataframe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next step
|
## Next Step
|
||||||
Now you have a perfect bot and want to control it from Telegram. Your
|
Now you have a perfect bot and want to control it from Telegram. Your
|
||||||
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# freqtrade documentation
|
# freqtrade documentation
|
||||||
|
|
||||||
Welcome to freqtrade documentation. Please feel free to contribute to
|
Welcome to freqtrade documentation. Please feel free to contribute to
|
||||||
this documentation if you see it became outdated by sending us a
|
this documentation if you see it became outdated by sending us a
|
||||||
Pull-request. Do not hesitate to reach us on
|
Pull-request. Do not hesitate to reach us on
|
||||||
@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on
|
|||||||
if you do not find the answer to your questions.
|
if you do not find the answer to your questions.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||||
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
|
||||||
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
|
||||||
@ -25,8 +27,10 @@ Pull-request. Do not hesitate to reach us on
|
|||||||
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
|
||||||
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
|
||||||
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
|
||||||
|
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
|
||||||
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||||
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
|
||||||
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
|
||||||
|
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md))
|
||||||
|
@ -56,23 +56,29 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev
|
|||||||
|
|
||||||
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
|
||||||
|
|
||||||
|
|
||||||
## Manual installation - Linux/MacOS
|
## Manual installation - Linux/MacOS
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:freqtrade/freqtrade.git
|
git clone git@github.com:freqtrade/freqtrade.git
|
||||||
git checkout develop
|
git checkout develop
|
||||||
cd freqtrade
|
cd freqtrade
|
||||||
```
|
```
|
||||||
**2. Create the config file**
|
|
||||||
|
### 2. Create the config file
|
||||||
|
|
||||||
Switch `"dry_run": true,`
|
Switch `"dry_run": true,`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp config.json.example config.json
|
cp config.json.example config.json
|
||||||
vi config.json
|
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 /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
@ -261,6 +267,7 @@ Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
|||||||
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
|
||||||
|
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||||
./configure --prefix=/usr
|
./configure --prefix=/usr
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
|
@ -24,7 +24,7 @@ script/plot_dataframe.py [-h] [-p pair] [--live]
|
|||||||
|
|
||||||
Example
|
Example
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -p BTC_ETH
|
python scripts/plot_dataframe.py -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
The `-p` pair argument, can be used to specify what
|
The `-p` pair argument, can be used to specify what
|
||||||
@ -34,18 +34,18 @@ pair you would like to plot.
|
|||||||
|
|
||||||
To plot the current live price use the `--live` flag:
|
To plot the current live price use the `--live` flag:
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -p BTC_ETH --live
|
python scripts/plot_dataframe.py -p BTC/ETH --live
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot a timerange (to zoom in):
|
To plot a timerange (to zoom in):
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
|
python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
|
||||||
```
|
```
|
||||||
Timerange doesn't work with live data.
|
Timerange doesn't work with live data.
|
||||||
|
|
||||||
To plot trades stored in a database use `--db-url` argument:
|
To plot trades stored in a database use `--db-url` argument:
|
||||||
```
|
```
|
||||||
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
|
python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH
|
||||||
```
|
```
|
||||||
|
|
||||||
To plot a test strategy the strategy should have first be backtested.
|
To plot a test strategy the strategy should have first be backtested.
|
||||||
|
151
docs/sandbox-testing.md
Normal file
151
docs/sandbox-testing.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Sandbox API testing
|
||||||
|
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
|
||||||
|
|
||||||
|
This document is a *light overview of configuring Freqtrade and GDAX sandbox.
|
||||||
|
This can be useful to developers and trader alike as Freqtrade is quite customisable.
|
||||||
|
|
||||||
|
When testing your API connectivity, make sure to use the following URLs.
|
||||||
|
***Website**
|
||||||
|
https://public.sandbox.gdax.com
|
||||||
|
***REST API**
|
||||||
|
https://api-public.sandbox.gdax.com
|
||||||
|
|
||||||
|
---
|
||||||
|
# Configure a Sandbox account on Gdax
|
||||||
|
Aim of this document section
|
||||||
|
- An sanbox account
|
||||||
|
- create 2FA (needed to create an API)
|
||||||
|
- Add test 50BTC to account
|
||||||
|
- Create :
|
||||||
|
- - API-KEY
|
||||||
|
- - API-Secret
|
||||||
|
- - API Password
|
||||||
|
|
||||||
|
## Acccount
|
||||||
|
|
||||||
|
This link will redirect to the sandbox main page to login / create account dialogues:
|
||||||
|
https://public.sandbox.pro.coinbase.com/orders/
|
||||||
|
|
||||||
|
After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar.
|
||||||
|
> https://public.sandbox.pro.coinbase.com/
|
||||||
|
|
||||||
|
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
|
||||||
|
From within sand box site select your profile, top right.
|
||||||
|
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
|
||||||
|
|
||||||
|
From the menu panel to the left of the screen select
|
||||||
|
> Security: "*View or Update*"
|
||||||
|
|
||||||
|
In the new site select "enable authenticator" as typical google Authenticator.
|
||||||
|
- open Google Authenticator on your phone
|
||||||
|
- scan barcode
|
||||||
|
- enter your generated 2fa
|
||||||
|
|
||||||
|
## Enable API Access
|
||||||
|
From within sandbox select profile>api>create api-keys
|
||||||
|
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
|
||||||
|
|
||||||
|
Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2Fa
|
||||||
|
- **Copy and paste the Passphase** into a notepade this will be needed later
|
||||||
|
- **Copy and paste the API Secret** popup into a notepad this will needed later
|
||||||
|
- **Copy and paste the API Key** into a notepad this will needed later
|
||||||
|
|
||||||
|
## Add 50 BTC test funds
|
||||||
|
To add funds, use the web interface deposit and withdraw buttons.
|
||||||
|
|
||||||
|
|
||||||
|
To begin select 'Wallets' from the top menu.
|
||||||
|
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
|
||||||
|
|
||||||
|
- Deposits (bottom left of screen)
|
||||||
|
- - Deposit Funds Bitcoin
|
||||||
|
- - - Coinbase BTC Wallet
|
||||||
|
- - - - Max (50 BTC)
|
||||||
|
- - - - - Deposit
|
||||||
|
|
||||||
|
*This process may be repeated for other currencies, ETH as example*
|
||||||
|
---
|
||||||
|
# Configure Freqtrade to use Gax Sandbox
|
||||||
|
|
||||||
|
The aim of this document section
|
||||||
|
- Enable sandbox URLs in Freqtrade
|
||||||
|
- Configure API
|
||||||
|
- - secret
|
||||||
|
- - key
|
||||||
|
- - passphrase
|
||||||
|
|
||||||
|
## Sandbox URLs
|
||||||
|
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
||||||
|
These include `['test']` and `['api']`.
|
||||||
|
- `[Test]` if available will point to an Exchanges sandbox.
|
||||||
|
- `[Api]` normally used, and resolves to live API target on the exchange
|
||||||
|
|
||||||
|
To make use of sandbox / test add "sandbox": true, to your config.json
|
||||||
|
```
|
||||||
|
"exchange": {
|
||||||
|
"name": "gdax",
|
||||||
|
"sandbox": true,
|
||||||
|
"key": "5wowfxemogxeowo;heiohgmd",
|
||||||
|
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||||
|
"password": "1bkjfkhfhfu6sr",
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC/USD"
|
||||||
|
```
|
||||||
|
Also insert your
|
||||||
|
- api-key (noted earlier)
|
||||||
|
- api-secret (noted earlier)
|
||||||
|
- password (the passphrase - noted earlier)
|
||||||
|
|
||||||
|
---
|
||||||
|
## You should now be ready to test your sandbox!
|
||||||
|
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox.
|
||||||
|
** Typically the BTC/USD has the most activity in sandbox to test against.
|
||||||
|
|
||||||
|
## GDAX - Old Candles problem
|
||||||
|
It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks
|
||||||
|
|
||||||
|
To disable this check, edit:
|
||||||
|
>strategy/interface.py
|
||||||
|
Look for the following section:
|
||||||
|
```
|
||||||
|
# Check if dataframe is out of date
|
||||||
|
signal_date = arrow.get(latest['date'])
|
||||||
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
|
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
||||||
|
logger.warning(
|
||||||
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
pair,
|
||||||
|
(arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
```
|
||||||
|
|
||||||
|
You could Hash out the entire check as follows:
|
||||||
|
```
|
||||||
|
# # Check if dataframe is out of date
|
||||||
|
# signal_date = arrow.get(latest['date'])
|
||||||
|
# interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
|
# if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
||||||
|
# logger.warning(
|
||||||
|
# 'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
# pair,
|
||||||
|
# (arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
# )
|
||||||
|
# return False, False
|
||||||
|
```
|
||||||
|
|
||||||
|
Or inrease the timeout to offer a level of protection/alignment of this test to freqtrade in live.
|
||||||
|
|
||||||
|
As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)"
|
||||||
|
```
|
||||||
|
# Check if dataframe is out of date
|
||||||
|
signal_date = arrow.get(latest['date'])
|
||||||
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
|
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))):
|
||||||
|
logger.warning(
|
||||||
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
pair,
|
||||||
|
(arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
```
|
@ -59,7 +59,7 @@ SELECT * FROM trades;
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
UPDATE trades
|
UPDATE trades
|
||||||
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate
|
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate-1
|
||||||
WHERE id=<trade_ID_to_update>;
|
WHERE id=<trade_ID_to_update>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
51
docs/stoploss.md
Normal file
51
docs/stoploss.md
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Stop Loss support
|
||||||
|
|
||||||
|
At this stage the bot contains the following stoploss support modes:
|
||||||
|
|
||||||
|
1. static stop loss, defined in either the strategy or configuration
|
||||||
|
2. trailing stop loss, defined in the configuration
|
||||||
|
3. trailing stop loss, custom positive loss, defined in configuration
|
||||||
|
|
||||||
|
## Static Stop Loss
|
||||||
|
|
||||||
|
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
|
||||||
|
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
|
||||||
|
|
||||||
|
## Trail Stop Loss
|
||||||
|
|
||||||
|
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
|
||||||
|
To enable this Feauture all you have to do is to define the configuration element:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"trailing_stop" : True
|
||||||
|
```
|
||||||
|
|
||||||
|
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases.
|
||||||
|
|
||||||
|
For example, simplified math,
|
||||||
|
|
||||||
|
* you buy an asset at a price of 100$
|
||||||
|
* your stop loss is defined at 2%
|
||||||
|
* which means your stop loss, gets triggered once your asset dropped below 98$
|
||||||
|
* assuming your asset now increases to 102$
|
||||||
|
* your stop loss, will now be 2% of 102$ or 99.96$
|
||||||
|
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
|
||||||
|
|
||||||
|
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price
|
||||||
|
|
||||||
|
### Custom positive loss
|
||||||
|
|
||||||
|
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage,
|
||||||
|
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit,
|
||||||
|
it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them.
|
||||||
|
|
||||||
|
Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
"trailing_stop_positive": 0.01,
|
||||||
|
"trailing_stop_positive_offset": 0.011,
|
||||||
|
```
|
||||||
|
|
||||||
|
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
|
||||||
|
|
||||||
|
You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.
|
74
docs/webhook-config.md
Normal file
74
docs/webhook-config.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Webhook usage
|
||||||
|
|
||||||
|
This page explains how to configure your bot to talk to webhooks.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`.
|
||||||
|
|
||||||
|
Sample configuration (tested using IFTTT).
|
||||||
|
|
||||||
|
```json
|
||||||
|
"webhook": {
|
||||||
|
"enabled": true,
|
||||||
|
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
|
||||||
|
"webhookbuy": {
|
||||||
|
"value1": "Buying {pair}",
|
||||||
|
"value2": "limit {limit:8f}",
|
||||||
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
|
},
|
||||||
|
"webhooksell": {
|
||||||
|
"value1": "Selling {pair}",
|
||||||
|
"value2": "limit {limit:8f}",
|
||||||
|
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||||
|
},
|
||||||
|
"webhookstatus": {
|
||||||
|
"value1": "Status: {status}",
|
||||||
|
"value2": "",
|
||||||
|
"value3": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
|
||||||
|
|
||||||
|
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
|
||||||
|
|
||||||
|
### Webhookbuy
|
||||||
|
|
||||||
|
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
|
||||||
|
Possible parameters are:
|
||||||
|
|
||||||
|
* exchange
|
||||||
|
* pair
|
||||||
|
* market_url
|
||||||
|
* limit
|
||||||
|
* stake_amount
|
||||||
|
* stake_amount_fiat
|
||||||
|
* stake_currency
|
||||||
|
* fiat_currency
|
||||||
|
|
||||||
|
### Webhooksell
|
||||||
|
|
||||||
|
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
|
||||||
|
Possible parameters are:
|
||||||
|
|
||||||
|
* exchange
|
||||||
|
* pair
|
||||||
|
* gain
|
||||||
|
* market_url
|
||||||
|
* limit
|
||||||
|
* amount
|
||||||
|
* open_rate
|
||||||
|
* current_rate
|
||||||
|
* profit_amount
|
||||||
|
* profit_percent
|
||||||
|
* profit_fiat
|
||||||
|
* stake_currency
|
||||||
|
* fiat_currency
|
||||||
|
|
||||||
|
### Webhookstatus
|
||||||
|
|
||||||
|
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||||
|
|
||||||
|
The only possible value here is `{status}`.
|
@ -1,5 +1,5 @@
|
|||||||
""" FreqTrade bot """
|
""" FreqTrade bot """
|
||||||
__version__ = '0.17.0'
|
__version__ = '0.17.2'
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(BaseException):
|
class DependencyException(BaseException):
|
||||||
|
@ -7,8 +7,8 @@ To launch Freqtrade as a module
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from freqtrade import main
|
|
||||||
|
|
||||||
|
from freqtrade import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main.set_loggers()
|
main.set_loggers()
|
||||||
|
@ -1,237 +0,0 @@
|
|||||||
"""
|
|
||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Dict, List, Tuple
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame, to_datetime
|
|
||||||
|
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
|
||||||
"""
|
|
||||||
Enum to distinguish between buy and sell signals
|
|
||||||
"""
|
|
||||||
BUY = "buy"
|
|
||||||
SELL = "sell"
|
|
||||||
|
|
||||||
|
|
||||||
class Analyze(object):
|
|
||||||
"""
|
|
||||||
Analyze class contains everything the bot need to determine if the situation is good for
|
|
||||||
buying or selling.
|
|
||||||
"""
|
|
||||||
def __init__(self, config: dict) -> None:
|
|
||||||
"""
|
|
||||||
Init Analyze
|
|
||||||
:param config: Bot configuration (use the one from Configuration())
|
|
||||||
"""
|
|
||||||
self.config = config
|
|
||||||
self.strategy: IStrategy = 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
|
|
||||||
"""
|
|
||||||
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|
||||||
frame = DataFrame(ticker, columns=cols)
|
|
||||||
|
|
||||||
frame['date'] = to_datetime(frame['date'],
|
|
||||||
unit='ms',
|
|
||||||
utc=True,
|
|
||||||
infer_datetime_format=True)
|
|
||||||
|
|
||||||
# group by index and aggregate results to eliminate duplicate ticks
|
|
||||||
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
|
||||||
'open': 'first',
|
|
||||||
'high': 'max',
|
|
||||||
'low': 'min',
|
|
||||||
'close': 'last',
|
|
||||||
'volume': 'max',
|
|
||||||
})
|
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
|
||||||
return frame
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
return self.strategy.populate_indicators(dataframe=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
|
|
||||||
"""
|
|
||||||
return self.strategy.populate_buy_trend(dataframe=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
|
|
||||||
"""
|
|
||||||
return self.strategy.populate_sell_trend(dataframe=dataframe)
|
|
||||||
|
|
||||||
def get_ticker_interval(self) -> str:
|
|
||||||
"""
|
|
||||||
Return ticker interval to use
|
|
||||||
:return: Ticker interval value to use
|
|
||||||
"""
|
|
||||||
return self.strategy.ticker_interval
|
|
||||||
|
|
||||||
def get_stoploss(self) -> float:
|
|
||||||
"""
|
|
||||||
Return stoploss to use
|
|
||||||
:return: Strategy stoploss value to use
|
|
||||||
"""
|
|
||||||
return self.strategy.stoploss
|
|
||||||
|
|
||||||
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
|
|
||||||
"""
|
|
||||||
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 = self.parse_ticker_dataframe(ticker_history)
|
|
||||||
dataframe = self.populate_indicators(dataframe)
|
|
||||||
dataframe = self.populate_buy_trend(dataframe)
|
|
||||||
dataframe = self.populate_sell_trend(dataframe)
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
|
|
||||||
"""
|
|
||||||
Calculates current signal based several technical analysis indicators
|
|
||||||
:param pair: pair in format ANT/BTC
|
|
||||||
:param interval: Interval to use (in min)
|
|
||||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
|
||||||
"""
|
|
||||||
ticker_hist = exchange.get_ticker_history(pair, interval)
|
|
||||||
if not ticker_hist:
|
|
||||||
logger.warning('Empty ticker history for pair %s', pair)
|
|
||||||
return False, False
|
|
||||||
|
|
||||||
try:
|
|
||||||
dataframe = self.analyze_ticker(ticker_hist)
|
|
||||||
except ValueError as error:
|
|
||||||
logger.warning(
|
|
||||||
'Unable to analyze ticker for pair %s: %s',
|
|
||||||
pair,
|
|
||||||
str(error)
|
|
||||||
)
|
|
||||||
return False, False
|
|
||||||
except Exception as error:
|
|
||||||
logger.exception(
|
|
||||||
'Unexpected error when analyzing ticker for pair %s: %s',
|
|
||||||
pair,
|
|
||||||
str(error)
|
|
||||||
)
|
|
||||||
return False, False
|
|
||||||
|
|
||||||
if dataframe.empty:
|
|
||||||
logger.warning('Empty dataframe for pair %s', pair)
|
|
||||||
return False, False
|
|
||||||
|
|
||||||
latest = dataframe.iloc[-1]
|
|
||||||
|
|
||||||
# Check if dataframe is out of date
|
|
||||||
signal_date = arrow.get(latest['date'])
|
|
||||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
|
||||||
if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))):
|
|
||||||
logger.warning(
|
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
|
||||||
pair,
|
|
||||||
(arrow.utcnow() - signal_date).seconds // 60
|
|
||||||
)
|
|
||||||
return False, False
|
|
||||||
|
|
||||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
|
||||||
logger.debug(
|
|
||||||
'trigger: %s (pair=%s) buy=%s sell=%s',
|
|
||||||
latest['date'],
|
|
||||||
pair,
|
|
||||||
str(buy),
|
|
||||||
str(sell)
|
|
||||||
)
|
|
||||||
return buy, sell
|
|
||||||
|
|
||||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
"""
|
|
||||||
current_profit = trade.calc_profit_percent(rate)
|
|
||||||
if self.stop_loss_reached(current_profit=current_profit):
|
|
||||||
return True
|
|
||||||
|
|
||||||
experimental = self.config.get('experimental', {})
|
|
||||||
|
|
||||||
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
|
||||||
logger.debug('Buy signal still active - not selling.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
|
||||||
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
|
||||||
logger.debug('Required profit reached. Selling..')
|
|
||||||
return True
|
|
||||||
|
|
||||||
if experimental.get('sell_profit_only', False):
|
|
||||||
logger.debug('Checking if trade is profitable..')
|
|
||||||
if trade.calc_profit(rate=rate) <= 0:
|
|
||||||
return False
|
|
||||||
if sell and not buy and experimental.get('use_sell_signal', False):
|
|
||||||
logger.debug('Sell signal received. Selling..')
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stop_loss_reached(self, current_profit: float) -> bool:
|
|
||||||
"""Based on current profit of the trade and configured stoploss, decides to sell or not"""
|
|
||||||
|
|
||||||
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
|
|
||||||
logger.debug('Stop loss hit.')
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
|
||||||
"""
|
|
||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
|
||||||
sell
|
|
||||||
:return True if bot should sell at current rate
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
|
||||||
"""
|
|
||||||
Creates a dataframe and populates indicators for given ticker data
|
|
||||||
"""
|
|
||||||
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
|
|
||||||
for pair, pair_data in tickerdata.items()}
|
|
@ -2,12 +2,12 @@
|
|||||||
This module contains the argument manager class
|
This module contains the argument manager class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import os
|
||||||
import re
|
import re
|
||||||
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from typing import List, Optional, NamedTuple
|
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
|
|
||||||
@ -63,11 +63,10 @@ class Arguments(object):
|
|||||||
"""
|
"""
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'-v', '--verbose',
|
'-v', '--verbose',
|
||||||
help='be verbose',
|
help='verbose mode (-vv for more, -vvv to get all messages)',
|
||||||
action='store_const',
|
action='count',
|
||||||
dest='loglevel',
|
dest='loglevel',
|
||||||
const=logging.DEBUG,
|
default=0,
|
||||||
default=logging.INFO,
|
|
||||||
)
|
)
|
||||||
self.parser.add_argument(
|
self.parser.add_argument(
|
||||||
'--version',
|
'--version',
|
||||||
@ -143,6 +142,16 @@ class Arguments(object):
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
dest='refresh_pairs',
|
dest='refresh_pairs',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--strategy-list',
|
||||||
|
help='Provide a commaseparated list of strategies to backtest '
|
||||||
|
'Please note that ticker-interval needs to be set either in config '
|
||||||
|
'or via command line. When using this together with --export trades, '
|
||||||
|
'the strategy-name is injected into the filename '
|
||||||
|
'(so backtest-data.json becomes backtest-data-DefaultStrategy.json',
|
||||||
|
nargs='+',
|
||||||
|
dest='strategy_list',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--export',
|
'--export',
|
||||||
help='export backtest results, argument are: trades\
|
help='export backtest results, argument are: trades\
|
||||||
@ -177,11 +186,22 @@ class Arguments(object):
|
|||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--realistic-simulation',
|
'--eps', '--enable-position-stacking',
|
||||||
help='uses max_open_trades from config to simulate real world limitations',
|
help='Allow buying the same pair multiple times (position stacking)',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
dest='realistic_simulation',
|
dest='position_stacking',
|
||||||
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--dmmp', '--disable-max-market-positions',
|
||||||
|
help='Disable applying `max_open_trades` during backtest '
|
||||||
|
'(same as setting `max_open_trades` to a very high number)',
|
||||||
|
action='store_false',
|
||||||
|
dest='use_max_market_positions',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--timerange',
|
'--timerange',
|
||||||
help='specify what timerange of data to use.',
|
help='specify what timerange of data to use.',
|
||||||
@ -334,3 +354,10 @@ class Arguments(object):
|
|||||||
nargs='+',
|
nargs='+',
|
||||||
dest='timeframes',
|
dest='timeframes',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.parser.add_argument(
|
||||||
|
'--erase',
|
||||||
|
help='Clean all existing data for the selected exchange/pairs/timeframes',
|
||||||
|
dest='erase',
|
||||||
|
action='store_true'
|
||||||
|
)
|
||||||
|
@ -1,21 +1,33 @@
|
|||||||
"""
|
"""
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from typing import Optional, Dict, Any
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import ccxt
|
||||||
from jsonschema import Draft4Validator, validate
|
from jsonschema import Draft4Validator, validate
|
||||||
from jsonschema.exceptions import ValidationError, best_match
|
from jsonschema.exceptions import ValidationError, best_match
|
||||||
import ccxt
|
|
||||||
|
|
||||||
from freqtrade import OperationalException, constants
|
from freqtrade import OperationalException, constants
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def set_loggers(log_level: int = 0) -> None:
|
||||||
|
"""
|
||||||
|
Set the logger level for Third party libs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
|
||||||
|
logging.getLogger('ccxt.base.exchange').setLevel(
|
||||||
|
logging.INFO if log_level <= 2 else logging.DEBUG)
|
||||||
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Configuration(object):
|
class Configuration(object):
|
||||||
"""
|
"""
|
||||||
Class to read and init the bot configuration
|
Class to read and init the bot configuration
|
||||||
@ -62,8 +74,8 @@ class Configuration(object):
|
|||||||
conf = json.load(file)
|
conf = json.load(file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Config file "{}" not found!'
|
f'Config file "{path}" not found!'
|
||||||
' Please create a config file or check whether it exists.'.format(path))
|
' Please create a config file or check whether it exists.')
|
||||||
|
|
||||||
if 'internals' not in conf:
|
if 'internals' not in conf:
|
||||||
conf['internals'] = {}
|
conf['internals'] = {}
|
||||||
@ -79,12 +91,15 @@ class Configuration(object):
|
|||||||
|
|
||||||
# Log level
|
# Log level
|
||||||
if 'loglevel' in self.args and self.args.loglevel:
|
if 'loglevel' in self.args and self.args.loglevel:
|
||||||
config.update({'loglevel': self.args.loglevel})
|
config.update({'verbosity': self.args.loglevel})
|
||||||
logging.basicConfig(
|
else:
|
||||||
level=config['loglevel'],
|
config.update({'verbosity': 0})
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
logging.basicConfig(
|
||||||
)
|
level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
|
||||||
logger.info('Log level set to %s', logging.getLevelName(config['loglevel']))
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
set_loggers(config['verbosity'])
|
||||||
|
logger.info('Verbosity set to %s', config['verbosity'])
|
||||||
|
|
||||||
# Add dynamic_whitelist if found
|
# Add dynamic_whitelist if found
|
||||||
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
||||||
@ -109,7 +124,7 @@ class Configuration(object):
|
|||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
logger.info('Dry run is disabled')
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
logger.info('Using DB: "{}"'.format(config['db_url']))
|
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
self.check_exchange(config)
|
self.check_exchange(config)
|
||||||
@ -142,11 +157,18 @@ class Configuration(object):
|
|||||||
config.update({'live': True})
|
config.update({'live': True})
|
||||||
logger.info('Parameter -l/--live detected ...')
|
logger.info('Parameter -l/--live detected ...')
|
||||||
|
|
||||||
# If --realistic-simulation is used we add it to the configuration
|
# If --enable-position-stacking is used we add it to the configuration
|
||||||
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
|
if 'position_stacking' in self.args and self.args.position_stacking:
|
||||||
config.update({'realistic_simulation': True})
|
config.update({'position_stacking': True})
|
||||||
logger.info('Parameter --realistic-simulation detected ...')
|
logger.info('Parameter --enable-position-stacking detected ...')
|
||||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
|
||||||
|
# If --disable-max-market-positions is used we add it to the configuration
|
||||||
|
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
|
||||||
|
config.update({'use_max_market_positions': False})
|
||||||
|
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||||
|
logger.info('max_open_trades set to unlimited ...')
|
||||||
|
else:
|
||||||
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
|
||||||
# If --timerange is used we add it to the configuration
|
# If --timerange is used we add it to the configuration
|
||||||
if 'timerange' in self.args and self.args.timerange:
|
if 'timerange' in self.args and self.args.timerange:
|
||||||
@ -165,6 +187,14 @@ class Configuration(object):
|
|||||||
config.update({'refresh_pairs': True})
|
config.update({'refresh_pairs': True})
|
||||||
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
||||||
|
|
||||||
|
if 'strategy_list' in self.args and self.args.strategy_list:
|
||||||
|
config.update({'strategy_list': self.args.strategy_list})
|
||||||
|
logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list))
|
||||||
|
|
||||||
|
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
||||||
|
config.update({'ticker_interval': self.args.ticker_interval})
|
||||||
|
logger.info('Overriding ticker interval with Command line argument')
|
||||||
|
|
||||||
# If --export is used we add it to the configuration
|
# If --export is used we add it to the configuration
|
||||||
if 'export' in self.args and self.args.export:
|
if 'export' in self.args and self.args.export:
|
||||||
config.update({'export': self.args.export})
|
config.update({'export': self.args.export})
|
||||||
@ -182,7 +212,7 @@ class Configuration(object):
|
|||||||
Extract information for sys.argv and load Hyperopt configuration
|
Extract information for sys.argv and load Hyperopt configuration
|
||||||
:return: configuration as dictionary
|
:return: configuration as dictionary
|
||||||
"""
|
"""
|
||||||
# If --realistic-simulation is used we add it to the configuration
|
# If --epochs is used we add it to the configuration
|
||||||
if 'epochs' in self.args and self.args.epochs:
|
if 'epochs' in self.args and self.args.epochs:
|
||||||
config.update({'epochs': self.args.epochs})
|
config.update({'epochs': self.args.epochs})
|
||||||
logger.info('Parameter --epochs detected ...')
|
logger.info('Parameter --epochs detected ...')
|
||||||
|
@ -36,7 +36,7 @@ SUPPORTED_FIAT = [
|
|||||||
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
|
||||||
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
|
||||||
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
|
||||||
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
|
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
@ -45,7 +45,7 @@ CONF_SCHEMA = {
|
|||||||
'properties': {
|
'properties': {
|
||||||
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
'max_open_trades': {'type': 'integer', 'minimum': 0},
|
||||||
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
|
||||||
'stake_amount': {
|
'stake_amount': {
|
||||||
"type": ["number", "string"],
|
"type": ["number", "string"],
|
||||||
"minimum": 0.0005,
|
"minimum": 0.0005,
|
||||||
@ -61,7 +61,16 @@ CONF_SCHEMA = {
|
|||||||
'minProperties': 1
|
'minProperties': 1
|
||||||
},
|
},
|
||||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
'trailing_stop': {'type': 'boolean'},
|
||||||
|
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
|
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
|
||||||
|
'unfilledtimeout': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'buy': {'type': 'number', 'minimum': 3},
|
||||||
|
'sell': {'type': 'number', 'minimum': 10}
|
||||||
|
}
|
||||||
|
},
|
||||||
'bid_strategy': {
|
'bid_strategy': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -92,6 +101,15 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['enabled', 'token', 'chat_id']
|
'required': ['enabled', 'token', 'chat_id']
|
||||||
},
|
},
|
||||||
|
'webhook': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'webhookbuy': {'type': 'object'},
|
||||||
|
'webhooksell': {'type': 'object'},
|
||||||
|
'webhookstatus': {'type': 'object'},
|
||||||
|
},
|
||||||
|
},
|
||||||
'db_url': {'type': 'string'},
|
'db_url': {'type': 'string'},
|
||||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||||
'internals': {
|
'internals': {
|
||||||
@ -107,8 +125,11 @@ CONF_SCHEMA = {
|
|||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'name': {'type': 'string'},
|
'name': {'type': 'string'},
|
||||||
|
'sandbox': {'type': 'boolean'},
|
||||||
'key': {'type': 'string'},
|
'key': {'type': 'string'},
|
||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
|
'password': {'type': 'string'},
|
||||||
|
'uid': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {
|
'items': {
|
||||||
@ -136,7 +157,6 @@ CONF_SCHEMA = {
|
|||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
'fiat_display_currency',
|
|
||||||
'dry_run',
|
'dry_run',
|
||||||
'bid_strategy',
|
'bid_strategy',
|
||||||
'telegram'
|
'telegram'
|
||||||
|
@ -4,6 +4,7 @@ import logging
|
|||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from math import floor, ceil
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import arrow
|
import arrow
|
||||||
@ -70,6 +71,10 @@ class Exchange(object):
|
|||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
if config.get('ticker_interval'):
|
||||||
|
# Check if timeframe is available
|
||||||
|
self.validate_timeframes(config['ticker_interval'])
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
@ -86,11 +91,13 @@ class Exchange(object):
|
|||||||
'secret': exchange_config.get('secret'),
|
'secret': exchange_config.get('secret'),
|
||||||
'password': exchange_config.get('password'),
|
'password': exchange_config.get('password'),
|
||||||
'uid': exchange_config.get('uid', ''),
|
'uid': exchange_config.get('uid', ''),
|
||||||
'enableRateLimit': True,
|
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True),
|
||||||
})
|
})
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
|
||||||
|
self.set_sandbox(api, exchange_config, name)
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -103,6 +110,16 @@ class Exchange(object):
|
|||||||
"""exchange ccxt id"""
|
"""exchange ccxt id"""
|
||||||
return self._api.id
|
return self._api.id
|
||||||
|
|
||||||
|
def set_sandbox(self, api, exchange_config: dict, name: str):
|
||||||
|
if exchange_config.get('sandbox'):
|
||||||
|
if api.urls.get('test'):
|
||||||
|
api.urls['api'] = api.urls['test']
|
||||||
|
logger.info("Enabled Sandbox API on %s", name)
|
||||||
|
else:
|
||||||
|
logger.warning(self._api.name, "No Sandbox URL in CCXT, exiting. "
|
||||||
|
"Please check your config.json")
|
||||||
|
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||||
|
|
||||||
def validate_pairs(self, pairs: List[str]) -> None:
|
def validate_pairs(self, pairs: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
@ -128,6 +145,15 @@ class Exchange(object):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available at {self.name}')
|
f'Pair {pair} is not available at {self.name}')
|
||||||
|
|
||||||
|
def validate_timeframes(self, timeframe: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Checks if ticker interval from config is a supported timeframe on the exchange
|
||||||
|
"""
|
||||||
|
timeframes = self._api.timeframes
|
||||||
|
if timeframe not in timeframes:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
@ -137,6 +163,28 @@ class Exchange(object):
|
|||||||
"""
|
"""
|
||||||
return endpoint in self._api.has and self._api.has[endpoint]
|
return endpoint in self._api.has and self._api.has[endpoint]
|
||||||
|
|
||||||
|
def symbol_amount_prec(self, pair, amount: float):
|
||||||
|
'''
|
||||||
|
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||||
|
Rounded down
|
||||||
|
'''
|
||||||
|
if self._api.markets[pair]['precision']['amount']:
|
||||||
|
symbol_prec = self._api.markets[pair]['precision']['amount']
|
||||||
|
big_amount = amount * pow(10, symbol_prec)
|
||||||
|
amount = floor(big_amount) / pow(10, symbol_prec)
|
||||||
|
return amount
|
||||||
|
|
||||||
|
def symbol_price_prec(self, pair, price: float):
|
||||||
|
'''
|
||||||
|
Returns the price buying or selling with to the precision the Exchange accepts
|
||||||
|
Rounds up
|
||||||
|
'''
|
||||||
|
if self._api.markets[pair]['precision']['price']:
|
||||||
|
symbol_prec = self._api.markets[pair]['precision']['price']
|
||||||
|
big_price = price * pow(10, symbol_prec)
|
||||||
|
price = ceil(big_price) / pow(10, symbol_prec)
|
||||||
|
return price
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
def buy(self, pair: str, rate: float, amount: float) -> Dict:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||||
@ -154,6 +202,10 @@ class Exchange(object):
|
|||||||
return {'id': order_id}
|
return {'id': order_id}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
|
rate = self.symbol_price_prec(pair, rate)
|
||||||
|
|
||||||
return self._api.create_limit_buy_order(pair, amount, rate)
|
return self._api.create_limit_buy_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
@ -187,6 +239,10 @@ class Exchange(object):
|
|||||||
return {'id': order_id}
|
return {'id': order_id}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
|
amount = self.symbol_amount_prec(pair, amount)
|
||||||
|
rate = self.symbol_price_prec(pair, rate)
|
||||||
|
|
||||||
return self._api.create_limit_sell_order(pair, amount, rate)
|
return self._api.create_limit_sell_order(pair, amount, rate)
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
@ -274,7 +330,7 @@ class Exchange(object):
|
|||||||
return self._cached_ticker[pair]
|
return self._cached_ticker[pair]
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_ticker_history(self, pair: str, tick_interval: str,
|
def get_candle_history(self, pair: str, tick_interval: str,
|
||||||
since_ms: Optional[int] = None) -> List[Dict]:
|
since_ms: Optional[int] = None) -> List[Dict]:
|
||||||
try:
|
try:
|
||||||
# last item should be in the time interval [now - tick_interval, now]
|
# last item should be in the time interval [now - tick_interval, now]
|
||||||
|
33
freqtrade/exchange/exchange_helpers.py
Normal file
33
freqtrade/exchange/exchange_helpers.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Analyses the trend for the given ticker history
|
||||||
|
:param ticker: See exchange.get_candle_history
|
||||||
|
:return: DataFrame
|
||||||
|
"""
|
||||||
|
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
frame = DataFrame(ticker, columns=cols)
|
||||||
|
|
||||||
|
frame['date'] = to_datetime(frame['date'],
|
||||||
|
unit='ms',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
|
# group by index and aggregate results to eliminate duplicate ticks
|
||||||
|
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
|
||||||
|
'open': 'first',
|
||||||
|
'high': 'max',
|
||||||
|
'low': 'min',
|
||||||
|
'close': 'last',
|
||||||
|
'volume': 'max',
|
||||||
|
})
|
||||||
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
|
return frame
|
@ -8,9 +8,10 @@ import time
|
|||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from coinmarketcap import Market
|
from coinmarketcap import Market
|
||||||
from requests.exceptions import RequestException
|
|
||||||
from freqtrade.constants import SUPPORTED_FIAT
|
from freqtrade.constants import SUPPORTED_FIAT
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -88,10 +89,10 @@ class CryptoToFiatConverter(object):
|
|||||||
coinlistings = self._coinmarketcap.listings()
|
coinlistings = self._coinmarketcap.listings()
|
||||||
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
|
||||||
coinlistings["data"]))
|
coinlistings["data"]))
|
||||||
except (ValueError, RequestException) as exception:
|
except (BaseException) as exception:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
"Could not load FIAT Cryptocurrency map for the following problem: %s",
|
||||||
exception
|
type(exception).__name__
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
||||||
|
@ -7,22 +7,20 @@ import logging
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List, Optional, Any, Callable
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
import requests
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
from freqtrade import (
|
from freqtrade import (DependencyException, OperationalException,
|
||||||
DependencyException, OperationalException, TemporaryError, persistence, __version__,
|
TemporaryError, __version__, constants, persistence)
|
||||||
)
|
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc_manager import RPCManager
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -50,12 +48,10 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# Init objects
|
# Init objects
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = Analyze(self.config)
|
self.strategy: IStrategy = StrategyResolver(self.config).strategy
|
||||||
self.fiat_converter = CryptoToFiatConverter()
|
|
||||||
self.rpc: RPCManager = RPCManager(self)
|
self.rpc: RPCManager = RPCManager(self)
|
||||||
self.persistence = None
|
self.persistence = None
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
|
|
||||||
self._init_modules()
|
self._init_modules()
|
||||||
|
|
||||||
def _init_modules(self) -> None:
|
def _init_modules(self) -> None:
|
||||||
@ -93,8 +89,13 @@ class FreqtradeBot(object):
|
|||||||
# Log state transition
|
# Log state transition
|
||||||
state = self.state
|
state = self.state
|
||||||
if state != old_state:
|
if state != old_state:
|
||||||
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'{state.name.lower()}'
|
||||||
|
})
|
||||||
logger.info('Changing state to: %s', state.name)
|
logger.info('Changing state to: %s', state.name)
|
||||||
|
if state == State.RUNNING:
|
||||||
|
self._startup_messages()
|
||||||
|
|
||||||
if state == State.STOPPED:
|
if state == State.STOPPED:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@ -111,6 +112,38 @@ class FreqtradeBot(object):
|
|||||||
nb_assets=nb_assets)
|
nb_assets=nb_assets)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
def _startup_messages(self) -> None:
|
||||||
|
if self.config.get('dry_run', False):
|
||||||
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
|
'status': 'Dry run is enabled. All trades are simulated.'
|
||||||
|
})
|
||||||
|
stake_currency = self.config['stake_currency']
|
||||||
|
stake_amount = self.config['stake_amount']
|
||||||
|
minimal_roi = self.config['minimal_roi']
|
||||||
|
ticker_interval = self.config['ticker_interval']
|
||||||
|
exchange_name = self.config['exchange']['name']
|
||||||
|
strategy_name = self.config.get('strategy', '')
|
||||||
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||||
|
'status': f'*Exchange:* `{exchange_name}`\n'
|
||||||
|
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
|
||||||
|
f'*Minimum ROI:* `{minimal_roi}`\n'
|
||||||
|
f'*Ticker Interval:* `{ticker_interval}`\n'
|
||||||
|
f'*Strategy:* `{strategy_name}`'
|
||||||
|
})
|
||||||
|
if self.config.get('dynamic_whitelist', False):
|
||||||
|
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
|
||||||
|
specific_pairs = ''
|
||||||
|
else:
|
||||||
|
top_pairs = 'whitelisted'
|
||||||
|
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
|
||||||
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
|
||||||
|
f'{specific_pairs}'
|
||||||
|
})
|
||||||
|
|
||||||
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Throttles the given callable that it
|
Throttles the given callable that it
|
||||||
@ -160,7 +193,7 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
if 'unfilledtimeout' in self.config:
|
if 'unfilledtimeout' in self.config:
|
||||||
# Check and handle any timed out open orders
|
# Check and handle any timed out open orders
|
||||||
self.check_handle_timedout(self.config['unfilledtimeout'])
|
self.check_handle_timedout()
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
except TemporaryError as error:
|
except TemporaryError as error:
|
||||||
@ -169,9 +202,10 @@ class FreqtradeBot(object):
|
|||||||
except OperationalException:
|
except OperationalException:
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
hint = 'Issue `/start` if you think it is safe to restart.'
|
hint = 'Issue `/start` if you think it is safe to restart.'
|
||||||
self.rpc.send_msg(
|
self.rpc.send_msg({
|
||||||
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
)
|
'status': f'OperationalException:\n```\n{tb}```{hint}'
|
||||||
|
})
|
||||||
logger.exception('OperationalException. Stopping trader ...')
|
logger.exception('OperationalException. Stopping trader ...')
|
||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
return state_changed
|
return state_changed
|
||||||
@ -245,6 +279,11 @@ class FreqtradeBot(object):
|
|||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
def _get_trade_stake_amount(self) -> Optional[float]:
|
def _get_trade_stake_amount(self) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
Check if stake amount can be fulfilled with the available balance
|
||||||
|
for the stake currency
|
||||||
|
:return: float: Stake Amount
|
||||||
|
"""
|
||||||
stake_amount = self.config['stake_amount']
|
stake_amount = self.config['stake_amount']
|
||||||
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
|
||||||
|
|
||||||
@ -277,18 +316,21 @@ class FreqtradeBot(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
min_stake_amounts = []
|
min_stake_amounts = []
|
||||||
if 'cost' in market['limits'] and 'min' in market['limits']['cost']:
|
limits = market['limits']
|
||||||
min_stake_amounts.append(market['limits']['cost']['min'])
|
if ('cost' in limits and 'min' in limits['cost']
|
||||||
|
and limits['cost']['min'] is not None):
|
||||||
|
min_stake_amounts.append(limits['cost']['min'])
|
||||||
|
|
||||||
if 'amount' in market['limits'] and 'min' in market['limits']['amount']:
|
if ('amount' in limits and 'min' in limits['amount']
|
||||||
min_stake_amounts.append(market['limits']['amount']['min'] * price)
|
and limits['amount']['min'] is not None):
|
||||||
|
min_stake_amounts.append(limits['amount']['min'] * price)
|
||||||
|
|
||||||
if not min_stake_amounts:
|
if not min_stake_amounts:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
|
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
|
||||||
if self.analyze.get_stoploss() is not None:
|
if self.strategy.stoploss is not None:
|
||||||
amount_reserve_percent += self.analyze.get_stoploss()
|
amount_reserve_percent += self.strategy.stoploss
|
||||||
# it should not be more than 50%
|
# it should not be more than 50%
|
||||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||||
return min(min_stake_amounts)/amount_reserve_percent
|
return min(min_stake_amounts)/amount_reserve_percent
|
||||||
@ -299,14 +341,11 @@ class FreqtradeBot(object):
|
|||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:return: True if a trade object has been created and persisted, False otherwise
|
:return: True if a trade object has been created and persisted, False otherwise
|
||||||
"""
|
"""
|
||||||
interval = self.analyze.get_ticker_interval()
|
interval = self.strategy.ticker_interval
|
||||||
stake_amount = self._get_trade_stake_amount()
|
stake_amount = self._get_trade_stake_amount()
|
||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
stake_currency = self.config['stake_currency']
|
|
||||||
fiat_currency = self.config['fiat_display_currency']
|
|
||||||
exc_name = self.exchange.name
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
'Checking buy signals to create a new trade with stake_amount: %f ...',
|
||||||
@ -325,14 +364,23 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
# Pick pair based on buy signals
|
# Pick pair based on buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
|
thistory = self.exchange.get_candle_history(_pair, interval)
|
||||||
|
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
pair = _pair
|
return self.execute_buy(_pair, stake_amount)
|
||||||
break
|
return False
|
||||||
else:
|
|
||||||
return False
|
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
||||||
|
"""
|
||||||
|
Executes a limit buy for the given pair
|
||||||
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
pair_s = pair.replace('_', '/')
|
pair_s = pair.replace('_', '/')
|
||||||
pair_url = self.exchange.get_pair_detail_url(pair)
|
pair_url = self.exchange.get_pair_detail_url(pair)
|
||||||
|
stake_currency = self.config['stake_currency']
|
||||||
|
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||||
|
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
||||||
@ -349,18 +397,16 @@ class FreqtradeBot(object):
|
|||||||
|
|
||||||
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
|
||||||
|
|
||||||
stake_amount_fiat = self.fiat_converter.convert_amount(
|
self.rpc.send_msg({
|
||||||
stake_amount,
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
stake_currency,
|
'exchange': self.exchange.name.capitalize(),
|
||||||
fiat_currency
|
'pair': pair_s,
|
||||||
)
|
'market_url': pair_url,
|
||||||
|
'limit': buy_limit,
|
||||||
# Create trade entity and return
|
'stake_amount': stake_amount,
|
||||||
self.rpc.send_msg(
|
'stake_currency': stake_currency,
|
||||||
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
|
'fiat_currency': fiat_currency
|
||||||
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
})
|
||||||
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
|
|
||||||
)
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -373,7 +419,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
open_rate_requested=buy_limit,
|
open_rate_requested=buy_limit,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=self.exchange.id,
|
exchange=self.exchange.id,
|
||||||
open_order_id=order_id
|
open_order_id=order_id,
|
||||||
|
strategy=self.strategy.get_strategy_name(),
|
||||||
|
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
|
||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
@ -483,22 +531,27 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||||
(buy, sell) = self.analyze.get_signal(self.exchange,
|
ticker = self.exchange.get_candle_history(trade.pair, self.strategy.ticker_interval)
|
||||||
trade.pair, self.analyze.get_ticker_interval())
|
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
||||||
|
ticker)
|
||||||
|
|
||||||
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
|
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
|
||||||
self.execute_sell(trade, current_rate)
|
if should_sell.sell_flag:
|
||||||
|
self.execute_sell(trade, current_rate, should_sell.sell_type)
|
||||||
return True
|
return True
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_handle_timedout(self, timeoutvalue: int) -> None:
|
def check_handle_timedout(self) -> None:
|
||||||
"""
|
"""
|
||||||
Check if any orders are timed out and cancel if neccessary
|
Check if any orders are timed out and cancel if neccessary
|
||||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
|
buy_timeout = self.config['unfilledtimeout']['buy']
|
||||||
|
sell_timeout = self.config['unfilledtimeout']['sell']
|
||||||
|
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||||
|
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||||
|
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||||
try:
|
try:
|
||||||
@ -521,10 +574,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
if int(order['remaining']) == 0:
|
if int(order['remaining']) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if order['side'] == 'buy' and ordertime < timeoutthreashold:
|
# Check if trade is still actually open
|
||||||
self.handle_timedout_limit_buy(trade, order)
|
if order['status'] == 'open':
|
||||||
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
|
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
|
||||||
self.handle_timedout_limit_sell(trade, order)
|
self.handle_timedout_limit_buy(trade, order)
|
||||||
|
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
|
||||||
|
self.handle_timedout_limit_sell(trade, order)
|
||||||
|
|
||||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||||
# it is conditionally called in the
|
# it is conditionally called in the
|
||||||
@ -540,7 +595,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
Trade.session.delete(trade)
|
Trade.session.delete(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
logger.info('Buy order timeout for %s.', trade)
|
logger.info('Buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
|
||||||
|
})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if trade is partially complete, edit the stake details for the trade
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
@ -549,7 +607,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
logger.info('Partial buy order timeout for %s.', trade)
|
||||||
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Remaining buy order for {pair_s} cancelled due to timeout'
|
||||||
|
})
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||||
@ -567,65 +628,59 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
|
|||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
|
self.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout'
|
||||||
|
})
|
||||||
logger.info('Sell order timeout for %s.', trade)
|
logger.info('Sell order timeout for %s.', trade)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_sell(self, trade: Trade, limit: float) -> None:
|
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a limit sell for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
:param limit: limit rate for the sell order
|
:param limit: limit rate for the sell order
|
||||||
|
:param sellreason: Reason the sell was triggered
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
exc = trade.exchange
|
|
||||||
pair = trade.pair
|
|
||||||
# Execute sell and update trade record
|
# Execute sell and update trade record
|
||||||
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
trade.close_rate_requested = limit
|
trade.close_rate_requested = limit
|
||||||
|
trade.sell_reason = sell_reason.value
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
|
||||||
profit_trade = trade.calc_profit(rate=limit)
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = trade.calc_profit_percent(limit)
|
profit_percent = trade.calc_profit_percent(limit)
|
||||||
pair_url = self.exchange.get_pair_detail_url(trade.pair)
|
pair_url = self.exchange.get_pair_detail_url(trade.pair)
|
||||||
gain = "profit" if fmt_exp_profit > 0 else "loss"
|
gain = "profit" if profit_percent > 0 else "loss"
|
||||||
|
|
||||||
message = f"*{exc}:* Selling\n" \
|
msg = {
|
||||||
f"*Current Pair:* [{pair}]({pair_url})\n" \
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
f"*Limit:* `{limit}`\n" \
|
'exchange': trade.exchange.capitalize(),
|
||||||
f"*Amount:* `{round(trade.amount, 8)}`\n" \
|
'pair': trade.pair,
|
||||||
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
|
'gain': gain,
|
||||||
f"*Current Rate:* `{current_rate:.8f}`\n" \
|
'market_url': pair_url,
|
||||||
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
|
'limit': limit,
|
||||||
""
|
'amount': trade.amount,
|
||||||
|
'open_rate': trade.open_rate,
|
||||||
|
'current_rate': current_rate,
|
||||||
|
'profit_amount': profit_trade,
|
||||||
|
'profit_percent': profit_percent,
|
||||||
|
}
|
||||||
|
|
||||||
# For regular case, when the configuration exists
|
# For regular case, when the configuration exists
|
||||||
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
|
||||||
stake = self.config['stake_currency']
|
stake_currency = self.config['stake_currency']
|
||||||
fiat = self.config['fiat_display_currency']
|
fiat_currency = self.config['fiat_display_currency']
|
||||||
fiat_converter = CryptoToFiatConverter()
|
msg.update({
|
||||||
profit_fiat = fiat_converter.convert_amount(
|
'stake_currency': stake_currency,
|
||||||
profit_trade,
|
'fiat_currency': fiat_currency,
|
||||||
stake,
|
})
|
||||||
fiat
|
|
||||||
)
|
|
||||||
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
|
|
||||||
f'` / {profit_fiat:.3f} {fiat})`'\
|
|
||||||
''
|
|
||||||
# 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
|
# Send the message
|
||||||
self.rpc.send_msg(message)
|
self.rpc.send_msg(msg)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from math import exp, pi, sqrt, cos
|
from math import cos, exp, pi, sqrt
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import talib as ta
|
import talib as ta
|
||||||
|
@ -10,9 +10,10 @@ from typing import List
|
|||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration, set_loggers
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
from freqtrade.rpc import RPCMessageType
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None:
|
|||||||
logger.exception('Fatal exception!')
|
logger.exception('Fatal exception!')
|
||||||
finally:
|
finally:
|
||||||
if freqtrade:
|
if freqtrade:
|
||||||
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
|
freqtrade.rpc.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'process died'
|
||||||
|
})
|
||||||
freqtrade.cleanup()
|
freqtrade.cleanup()
|
||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
@ -73,24 +77,13 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
|
|||||||
|
|
||||||
# Create new instance
|
# Create new instance
|
||||||
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
freqtrade = FreqtradeBot(Configuration(args).get_config())
|
||||||
freqtrade.rpc.send_msg(
|
freqtrade.rpc.send_msg({
|
||||||
'*Status:* `Config reloaded ...`'.format(
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
freqtrade.state.name.lower()
|
'status': 'config reloaded'
|
||||||
)
|
})
|
||||||
)
|
|
||||||
return freqtrade
|
return freqtrade
|
||||||
|
|
||||||
|
|
||||||
def set_loggers() -> None:
|
|
||||||
"""
|
|
||||||
Set the logger level for Third party libs
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
|
||||||
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
|
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
set_loggers()
|
set_loggers()
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
Various tool function for Freqtrade and scripts
|
Various tool function for Freqtrade and scripts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import gzip
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
@ -54,11 +54,8 @@ def load_tickerdata_file(
|
|||||||
:return dict OR empty if unsuccesful
|
:return dict OR empty if unsuccesful
|
||||||
"""
|
"""
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
pair_file_string = pair.replace('/', '_')
|
pair_s = pair.replace('/', '_')
|
||||||
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
|
file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
|
||||||
pair=pair_file_string,
|
|
||||||
ticker_interval=ticker_interval,
|
|
||||||
))
|
|
||||||
gzipfile = file + '.gz'
|
gzipfile = file + '.gz'
|
||||||
|
|
||||||
# If the file does not exist we download it when None is returned.
|
# If the file does not exist we download it when None is returned.
|
||||||
@ -222,7 +219,7 @@ def download_backtesting_testdata(datadir: str,
|
|||||||
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
|
||||||
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||||
|
|
||||||
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
|
new_data = exchange.get_candle_history(pair=pair, tick_interval=tick_interval,
|
||||||
since_ms=since_ms)
|
since_ms=since_ms)
|
||||||
data.extend(new_data)
|
data.extend(new_data)
|
||||||
|
|
||||||
|
@ -6,21 +6,24 @@ This module contains the backtesting logic
|
|||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from datetime import datetime
|
from copy import deepcopy
|
||||||
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import constants, DependencyException
|
from freqtrade import DependencyException, constants
|
||||||
from freqtrade.exchange import Exchange
|
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.misc import file_dump_json
|
from freqtrade.misc import file_dump_json
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,6 +41,9 @@ class BacktestResult(NamedTuple):
|
|||||||
close_index: int
|
close_index: int
|
||||||
trade_duration: float
|
trade_duration: float
|
||||||
open_at_end: bool
|
open_at_end: bool
|
||||||
|
open_rate: float
|
||||||
|
close_rate: float
|
||||||
|
sell_reason: SellType
|
||||||
|
|
||||||
|
|
||||||
class Backtesting(object):
|
class Backtesting(object):
|
||||||
@ -48,13 +54,9 @@ class Backtesting(object):
|
|||||||
backtesting = Backtesting(config)
|
backtesting = Backtesting(config)
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.analyze = Analyze(self.config)
|
|
||||||
self.ticker_interval = self.analyze.strategy.ticker_interval
|
|
||||||
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
|
|
||||||
self.populate_buy_trend = self.analyze.populate_buy_trend
|
|
||||||
self.populate_sell_trend = self.analyze.populate_sell_trend
|
|
||||||
|
|
||||||
# Reset keys for backtesting
|
# Reset keys for backtesting
|
||||||
self.config['exchange']['key'] = ''
|
self.config['exchange']['key'] = ''
|
||||||
@ -62,9 +64,36 @@ class Backtesting(object):
|
|||||||
self.config['exchange']['password'] = ''
|
self.config['exchange']['password'] = ''
|
||||||
self.config['exchange']['uid'] = ''
|
self.config['exchange']['uid'] = ''
|
||||||
self.config['dry_run'] = True
|
self.config['dry_run'] = True
|
||||||
|
self.strategylist: List[IStrategy] = []
|
||||||
|
if self.config.get('strategy_list', None):
|
||||||
|
# Force one interval
|
||||||
|
self.ticker_interval = str(self.config.get('ticker_interval'))
|
||||||
|
for strat in list(self.config['strategy_list']):
|
||||||
|
stratconf = deepcopy(self.config)
|
||||||
|
stratconf['strategy'] = strat
|
||||||
|
self.strategylist.append(StrategyResolver(stratconf).strategy)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# only one strategy
|
||||||
|
strat = StrategyResolver(self.config).strategy
|
||||||
|
|
||||||
|
self.strategylist.append(StrategyResolver(self.config).strategy)
|
||||||
|
# Load one strategy
|
||||||
|
self._set_strategy(self.strategylist[0])
|
||||||
|
|
||||||
self.exchange = Exchange(self.config)
|
self.exchange = Exchange(self.config)
|
||||||
self.fee = self.exchange.get_fee()
|
self.fee = self.exchange.get_fee()
|
||||||
|
|
||||||
|
def _set_strategy(self, strategy):
|
||||||
|
"""
|
||||||
|
Load strategy into backtesting
|
||||||
|
"""
|
||||||
|
self.strategy = strategy
|
||||||
|
self.ticker_interval = self.config.get('ticker_interval')
|
||||||
|
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
|
||||||
|
self.advise_buy = strategy.advise_buy
|
||||||
|
self.advise_sell = strategy.advise_sell
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
"""
|
"""
|
||||||
@ -73,7 +102,7 @@ class Backtesting(object):
|
|||||||
:return: tuple containing min_date, max_date
|
:return: tuple containing min_date, max_date
|
||||||
"""
|
"""
|
||||||
timeframe = [
|
timeframe = [
|
||||||
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
|
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
|
||||||
for frame in data.values()
|
for frame in data.values()
|
||||||
]
|
]
|
||||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||||
@ -86,9 +115,9 @@ class Backtesting(object):
|
|||||||
"""
|
"""
|
||||||
stake_currency = str(self.config.get('stake_currency'))
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['pair', 'buy count', 'avg profit %',
|
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results.pair == pair]
|
||||||
@ -96,8 +125,10 @@ class Backtesting(object):
|
|||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
result.profit_percent.mean() * 100.0,
|
result.profit_percent.mean() * 100.0,
|
||||||
|
result.profit_percent.sum() * 100.0,
|
||||||
result.profit_abs.sum(),
|
result.profit_abs.sum(),
|
||||||
result.trade_duration.mean(),
|
str(timedelta(
|
||||||
|
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
|
||||||
len(result[result.profit_abs > 0]),
|
len(result[result.profit_abs > 0]),
|
||||||
len(result[result.profit_abs < 0])
|
len(result[result.profit_abs < 0])
|
||||||
])
|
])
|
||||||
@ -107,22 +138,63 @@ class Backtesting(object):
|
|||||||
'TOTAL',
|
'TOTAL',
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit_percent.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
|
results.profit_percent.sum() * 100.0,
|
||||||
results.profit_abs.sum(),
|
results.profit_abs.sum(),
|
||||||
results.trade_duration.mean(),
|
str(timedelta(
|
||||||
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
len(results[results.profit_abs > 0]),
|
len(results[results.profit_abs > 0]),
|
||||||
len(results[results.profit_abs < 0])
|
len(results[results.profit_abs < 0])
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
|
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
||||||
|
"""
|
||||||
|
Generate small table outlining Backtest results
|
||||||
|
"""
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['Sell Reason', 'Count']
|
||||||
|
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||||
|
tabular_data.append([reason.value, count])
|
||||||
|
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||||
|
|
||||||
records = [(trade_entry.pair, trade_entry.profit_percent,
|
def _generate_text_table_strategy(self, all_results: dict) -> str:
|
||||||
trade_entry.open_time.timestamp(),
|
"""
|
||||||
trade_entry.close_time.timestamp(),
|
Generate summary table per strategy
|
||||||
trade_entry.open_index - 1, trade_entry.trade_duration)
|
"""
|
||||||
for index, trade_entry in results.iterrows()]
|
stake_currency = str(self.config.get('stake_currency'))
|
||||||
|
|
||||||
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
||||||
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
|
for strategy, results in all_results.items():
|
||||||
|
tabular_data.append([
|
||||||
|
strategy,
|
||||||
|
len(results.index),
|
||||||
|
results.profit_percent.mean() * 100.0,
|
||||||
|
results.profit_percent.sum() * 100.0,
|
||||||
|
results.profit_abs.sum(),
|
||||||
|
str(timedelta(
|
||||||
|
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
|
||||||
|
len(results[results.profit_abs > 0]),
|
||||||
|
len(results[results.profit_abs < 0])
|
||||||
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
|
||||||
|
|
||||||
|
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
|
||||||
|
strategyname: Optional[str] = None) -> None:
|
||||||
|
|
||||||
|
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
|
||||||
|
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
|
||||||
|
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
|
||||||
|
for index, t in results.iterrows()]
|
||||||
|
|
||||||
if records:
|
if records:
|
||||||
|
if strategyname:
|
||||||
|
# Inject strategyname to filename
|
||||||
|
recname = Path(recordfilename)
|
||||||
|
recordfilename = str(Path.joinpath(
|
||||||
|
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
|
||||||
logger.info('Dumping backtest results to %s', recordfilename)
|
logger.info('Dumping backtest results to %s', recordfilename)
|
||||||
file_dump_json(recordfilename, records)
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
@ -133,7 +205,7 @@ class Backtesting(object):
|
|||||||
stake_amount = args['stake_amount']
|
stake_amount = args['stake_amount']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=buy_row.close,
|
open_rate=buy_row.open,
|
||||||
open_date=buy_row.date,
|
open_date=buy_row.date,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=stake_amount / buy_row.open,
|
amount=stake_amount / buy_row.open,
|
||||||
@ -148,31 +220,40 @@ class Backtesting(object):
|
|||||||
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
|
||||||
|
|
||||||
buy_signal = sell_row.buy
|
buy_signal = sell_row.buy
|
||||||
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
|
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
|
||||||
sell_row.sell):
|
sell_row.sell)
|
||||||
|
if sell.sell_flag:
|
||||||
|
|
||||||
return BacktestResult(pair=pair,
|
return BacktestResult(pair=pair,
|
||||||
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||||
profit_abs=trade.calc_profit(rate=sell_row.close),
|
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||||
open_time=buy_row.date,
|
open_time=buy_row.date,
|
||||||
close_time=sell_row.date,
|
close_time=sell_row.date,
|
||||||
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
trade_duration=int((
|
||||||
|
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||||
open_index=buy_row.Index,
|
open_index=buy_row.Index,
|
||||||
close_index=sell_row.Index,
|
close_index=sell_row.Index,
|
||||||
open_at_end=False
|
open_at_end=False,
|
||||||
|
open_rate=buy_row.open,
|
||||||
|
close_rate=sell_row.open,
|
||||||
|
sell_reason=sell.sell_type
|
||||||
)
|
)
|
||||||
if partial_ticker:
|
if partial_ticker:
|
||||||
# no sell condition found - trade stil open at end of backtest period
|
# no sell condition found - trade stil open at end of backtest period
|
||||||
sell_row = partial_ticker[-1]
|
sell_row = partial_ticker[-1]
|
||||||
btr = BacktestResult(pair=pair,
|
btr = BacktestResult(pair=pair,
|
||||||
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
|
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
|
||||||
profit_abs=trade.calc_profit(rate=sell_row.close),
|
profit_abs=trade.calc_profit(rate=sell_row.open),
|
||||||
open_time=buy_row.date,
|
open_time=buy_row.date,
|
||||||
close_time=sell_row.date,
|
close_time=sell_row.date,
|
||||||
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
|
trade_duration=int((
|
||||||
|
sell_row.date - buy_row.date).total_seconds() // 60),
|
||||||
open_index=buy_row.Index,
|
open_index=buy_row.Index,
|
||||||
close_index=sell_row.Index,
|
close_index=sell_row.Index,
|
||||||
open_at_end=True
|
open_at_end=True,
|
||||||
|
open_rate=buy_row.open,
|
||||||
|
close_rate=sell_row.open,
|
||||||
|
sell_reason=SellType.FORCE_SELL
|
||||||
)
|
)
|
||||||
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
|
||||||
btr.profit_percent, btr.profit_abs)
|
btr.profit_percent, btr.profit_abs)
|
||||||
@ -191,20 +272,20 @@ class Backtesting(object):
|
|||||||
stake_amount: btc amount to use for each trade
|
stake_amount: btc amount to use for each trade
|
||||||
processed: a processed dictionary with format {pair, data}
|
processed: a processed dictionary with format {pair, data}
|
||||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
realistic: do we try to simulate realistic trades? (default: True)
|
position_stacking: do we allow position stacking? (default: False)
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
headers = ['date', 'buy', 'open', 'close', 'sell']
|
headers = ['date', 'buy', 'open', 'close', 'sell']
|
||||||
processed = args['processed']
|
processed = args['processed']
|
||||||
max_open_trades = args.get('max_open_trades', 0)
|
max_open_trades = args.get('max_open_trades', 0)
|
||||||
realistic = args.get('realistic', False)
|
position_stacking = args.get('position_stacking', False)
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock: Dict = {}
|
trade_count_lock: Dict = {}
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
|
||||||
|
|
||||||
ticker_data = self.populate_sell_trend(
|
ticker_data = self.advise_sell(
|
||||||
self.populate_buy_trend(pair_data))[headers].copy()
|
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
|
||||||
|
|
||||||
# to avoid using data from future, we buy/sell with signal from previous candle
|
# to avoid using data from future, we buy/sell with signal from previous candle
|
||||||
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
|
||||||
@ -221,7 +302,7 @@ class Backtesting(object):
|
|||||||
if row.buy == 0 or row.sell == 1:
|
if row.buy == 0 or row.sell == 1:
|
||||||
continue # skip rows where no buy signal or that would immediately sell off
|
continue # skip rows where no buy signal or that would immediately sell off
|
||||||
|
|
||||||
if realistic:
|
if not position_stacking:
|
||||||
if lock_pair_until is not None and row.date <= lock_pair_until:
|
if lock_pair_until is not None and row.date <= lock_pair_until:
|
||||||
continue
|
continue
|
||||||
if max_open_trades > 0:
|
if max_open_trades > 0:
|
||||||
@ -257,7 +338,7 @@ class Backtesting(object):
|
|||||||
if self.config.get('live'):
|
if self.config.get('live'):
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
|
data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval)
|
||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
@ -275,58 +356,61 @@ class Backtesting(object):
|
|||||||
if not data:
|
if not data:
|
||||||
logger.critical("No data found. Terminating.")
|
logger.critical("No data found. Terminating.")
|
||||||
return
|
return
|
||||||
# Ignore max_open_trades in backtesting, except realistic flag was passed
|
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
|
||||||
if self.config.get('realistic_simulation', False):
|
if self.config.get('use_max_market_positions', True):
|
||||||
max_open_trades = self.config['max_open_trades']
|
max_open_trades = self.config['max_open_trades']
|
||||||
else:
|
else:
|
||||||
logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
|
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
|
||||||
max_open_trades = 0
|
max_open_trades = 0
|
||||||
|
all_results = {}
|
||||||
|
|
||||||
preprocessed = self.tickerdata_to_dataframe(data)
|
for strat in self.strategylist:
|
||||||
|
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||||
|
self._set_strategy(strat)
|
||||||
|
|
||||||
# Print timeframe
|
# need to reprocess data every time to populate signals
|
||||||
min_date, max_date = self.get_timeframe(preprocessed)
|
preprocessed = self.tickerdata_to_dataframe(data)
|
||||||
logger.info(
|
|
||||||
'Measuring data from %s up to %s (%s days)..',
|
|
||||||
min_date.isoformat(),
|
|
||||||
max_date.isoformat(),
|
|
||||||
(max_date - min_date).days
|
|
||||||
)
|
|
||||||
|
|
||||||
# Execute backtest and print results
|
# Print timeframe
|
||||||
results = self.backtest(
|
min_date, max_date = self.get_timeframe(preprocessed)
|
||||||
{
|
logger.info(
|
||||||
'stake_amount': self.config.get('stake_amount'),
|
'Measuring data from %s up to %s (%s days)..',
|
||||||
'processed': preprocessed,
|
min_date.isoformat(),
|
||||||
'max_open_trades': max_open_trades,
|
max_date.isoformat(),
|
||||||
'realistic': self.config.get('realistic_simulation', False),
|
(max_date - min_date).days
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.config.get('export', False):
|
|
||||||
self._store_backtest_result(self.config.get('exportfilename'), results)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'\n======================================== '
|
|
||||||
'BACKTESTING REPORT'
|
|
||||||
' =========================================\n'
|
|
||||||
'%s',
|
|
||||||
self._generate_text_table(
|
|
||||||
data,
|
|
||||||
results
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
# Execute backtest and print results
|
||||||
'\n====================================== '
|
all_results[self.strategy.get_strategy_name()] = self.backtest(
|
||||||
'LEFT OPEN TRADES REPORT'
|
{
|
||||||
' ======================================\n'
|
'stake_amount': self.config.get('stake_amount'),
|
||||||
'%s',
|
'processed': preprocessed,
|
||||||
self._generate_text_table(
|
'max_open_trades': max_open_trades,
|
||||||
data,
|
'position_stacking': self.config.get('position_stacking', False),
|
||||||
results.loc[results.open_at_end]
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
for strategy, results in all_results.items():
|
||||||
|
|
||||||
|
if self.config.get('export', False):
|
||||||
|
self._store_backtest_result(self.config['exportfilename'], results,
|
||||||
|
strategy if len(self.strategylist) > 1 else None)
|
||||||
|
|
||||||
|
print(f"Result for strategy {strategy}")
|
||||||
|
print(' BACKTESTING REPORT '.center(119, '='))
|
||||||
|
print(self._generate_text_table(data, results))
|
||||||
|
|
||||||
|
print(' SELL REASON STATS '.center(119, '='))
|
||||||
|
print(self._generate_text_table_sell_reason(data, results))
|
||||||
|
|
||||||
|
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
|
||||||
|
print(self._generate_text_table(data, results.loc[results.open_at_end]))
|
||||||
|
print()
|
||||||
|
if len(all_results) > 1:
|
||||||
|
# Print Strategy summary table
|
||||||
|
print(' Strategy Summary '.center(119, '='))
|
||||||
|
print(self._generate_text_table_strategy(all_results))
|
||||||
|
print('\nFor more details, please look at the detail tables above')
|
||||||
|
|
||||||
|
|
||||||
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
def setup_configuration(args: Namespace) -> Dict[str, Any]:
|
||||||
|
@ -4,22 +4,21 @@
|
|||||||
This module contains the hyperopt logic
|
This module contains the hyperopt logic
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import pickle
|
|
||||||
import signal
|
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
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, Optional
|
from typing import Any, Callable, Dict, List
|
||||||
|
|
||||||
import numpy
|
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from sklearn.externals.joblib import Parallel, delayed, dump, load
|
||||||
|
from skopt import Optimizer
|
||||||
|
from skopt.space import Categorical, Dimension, Integer, Real
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
@ -29,6 +28,9 @@ from freqtrade.optimize.backtesting import Backtesting
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
|
||||||
|
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
|
||||||
|
|
||||||
|
|
||||||
class Hyperopt(Backtesting):
|
class Hyperopt(Backtesting):
|
||||||
"""
|
"""
|
||||||
@ -44,7 +46,6 @@ class Hyperopt(Backtesting):
|
|||||||
# to the number of days
|
# to the number of days
|
||||||
self.target_trades = 600
|
self.target_trades = 600
|
||||||
self.total_tries = config.get('epochs', 0)
|
self.total_tries = config.get('epochs', 0)
|
||||||
self.current_tries = 0
|
|
||||||
self.current_best_loss = 100
|
self.current_best_loss = 100
|
||||||
|
|
||||||
# max average trade duration in minutes
|
# max average trade duration in minutes
|
||||||
@ -56,130 +57,38 @@ class Hyperopt(Backtesting):
|
|||||||
# check that the reported Σ% values do not exceed this!
|
# check that the reported Σ% values do not exceed this!
|
||||||
self.expected_max_profit = 3.0
|
self.expected_max_profit = 3.0
|
||||||
|
|
||||||
# Configuration and data used by hyperopt
|
# Previous evaluations
|
||||||
self.processed: Optional[Dict[str, Any]] = None
|
self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
|
||||||
|
self.trials: List = []
|
||||||
|
|
||||||
# Hyperopt Trials
|
def get_args(self, params):
|
||||||
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
|
dimensions = self.hyperopt_space()
|
||||||
self.trials = Trials()
|
# Ensure the number of dimensions match
|
||||||
|
# the number of parameters in the list x.
|
||||||
|
if len(params) != len(dimensions):
|
||||||
|
raise ValueError('Mismatch in number of search-space dimensions. '
|
||||||
|
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
|
||||||
|
|
||||||
|
# Create a dict where the keys are the names of the dimensions
|
||||||
|
# and the values are taken from the list of parameters x.
|
||||||
|
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
|
||||||
|
return arg_dict
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
|
||||||
Adds several different TA indicators to the given DataFrame
|
|
||||||
"""
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
|
||||||
dataframe['cci'] = ta.CCI(dataframe)
|
|
||||||
macd = ta.MACD(dataframe)
|
macd = ta.MACD(dataframe)
|
||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
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)
|
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)
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
# Stoch RSI
|
|
||||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
|
||||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
|
||||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
|
||||||
# Bollinger bands
|
# Bollinger bands
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
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)
|
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']
|
|
||||||
|
|
||||||
# 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
|
return dataframe
|
||||||
|
|
||||||
@ -187,15 +96,16 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Save hyperopt trials to file
|
Save hyperopt trials to file
|
||||||
"""
|
"""
|
||||||
logger.info('Saving Trials to \'%s\'', self.trials_file)
|
if self.trials:
|
||||||
pickle.dump(self.trials, open(self.trials_file, 'wb'))
|
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
|
||||||
|
dump(self.trials, self.trials_file)
|
||||||
|
|
||||||
def read_trials(self) -> Trials:
|
def read_trials(self) -> List:
|
||||||
"""
|
"""
|
||||||
Read hyperopt trials file
|
Read hyperopt trials file
|
||||||
"""
|
"""
|
||||||
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
logger.info('Reading Trials from \'%s\'', self.trials_file)
|
||||||
trials = pickle.load(open(self.trials_file, 'rb'))
|
trials = load(self.trials_file)
|
||||||
os.remove(self.trials_file)
|
os.remove(self.trials_file)
|
||||||
return trials
|
return trials
|
||||||
|
|
||||||
@ -203,22 +113,27 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Display Best hyperopt result
|
Display Best hyperopt result
|
||||||
"""
|
"""
|
||||||
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4)
|
results = sorted(self.trials, key=itemgetter('loss'))
|
||||||
results = self.trials.best_trial['result']['result']
|
best_result = results[0]
|
||||||
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
|
logger.info(
|
||||||
|
'Best result:\n%s\nwith values:\n%s',
|
||||||
|
best_result['result'],
|
||||||
|
best_result['params']
|
||||||
|
)
|
||||||
|
if 'roi_t1' in best_result['params']:
|
||||||
|
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
|
||||||
|
|
||||||
def log_results(self, results) -> None:
|
def log_results(self, results) -> None:
|
||||||
"""
|
"""
|
||||||
Log results if it is better than any previous evaluation
|
Log results if it is better than any previous evaluation
|
||||||
"""
|
"""
|
||||||
if results['loss'] < self.current_best_loss:
|
if results['loss'] < self.current_best_loss:
|
||||||
|
current = results['current_tries']
|
||||||
|
total = results['total_tries']
|
||||||
|
res = results['result']
|
||||||
|
loss = results['loss']
|
||||||
self.current_best_loss = results['loss']
|
self.current_best_loss = results['loss']
|
||||||
log_msg = '\n{:5d}/{}: {}. Loss {:.5f}'.format(
|
log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
|
||||||
results['current_tries'],
|
|
||||||
results['total_tries'],
|
|
||||||
results['result'],
|
|
||||||
results['loss']
|
|
||||||
)
|
|
||||||
print(log_msg)
|
print(log_msg)
|
||||||
else:
|
else:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
@ -231,7 +146,8 @@ class Hyperopt(Backtesting):
|
|||||||
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
|
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)
|
profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
|
||||||
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
|
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
|
||||||
return trade_loss + profit_loss + duration_loss
|
result = trade_loss + profit_loss + duration_loss
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
@ -247,87 +163,44 @@ class Hyperopt(Backtesting):
|
|||||||
return roi_table
|
return roi_table
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def roi_space() -> Dict[str, Any]:
|
def roi_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Values to search for each ROI steps
|
Values to search for each ROI steps
|
||||||
"""
|
"""
|
||||||
return {
|
return [
|
||||||
'roi_t1': hp.quniform('roi_t1', 10, 120, 20),
|
Integer(10, 120, name='roi_t1'),
|
||||||
'roi_t2': hp.quniform('roi_t2', 10, 60, 15),
|
Integer(10, 60, name='roi_t2'),
|
||||||
'roi_t3': hp.quniform('roi_t3', 10, 40, 10),
|
Integer(10, 40, name='roi_t3'),
|
||||||
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01),
|
Real(0.01, 0.04, name='roi_p1'),
|
||||||
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01),
|
Real(0.01, 0.07, name='roi_p2'),
|
||||||
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01),
|
Real(0.01, 0.20, name='roi_p3'),
|
||||||
}
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stoploss_space() -> Dict[str, Any]:
|
def stoploss_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Stoploss Value to search
|
Stoploss search space
|
||||||
"""
|
"""
|
||||||
return {
|
return [
|
||||||
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02),
|
Real(-0.5, -0.02, name='stoploss'),
|
||||||
}
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def indicator_space() -> Dict[str, Any]:
|
def indicator_space() -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Define your Hyperopt space for searching strategy parameters
|
Define your Hyperopt space for searching strategy parameters
|
||||||
"""
|
"""
|
||||||
return {
|
return [
|
||||||
'macd_below_zero': hp.choice('macd_below_zero', [
|
Integer(10, 25, name='mfi-value'),
|
||||||
{'enabled': False},
|
Integer(15, 45, name='fastd-value'),
|
||||||
{'enabled': True}
|
Integer(20, 50, name='adx-value'),
|
||||||
]),
|
Integer(20, 40, name='rsi-value'),
|
||||||
'mfi': hp.choice('mfi', [
|
Categorical([True, False], name='mfi-enabled'),
|
||||||
{'enabled': False},
|
Categorical([True, False], name='fastd-enabled'),
|
||||||
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)}
|
Categorical([True, False], name='adx-enabled'),
|
||||||
]),
|
Categorical([True, False], name='rsi-enabled'),
|
||||||
'fastd': hp.choice('fastd', [
|
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
|
||||||
{'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:
|
def has_space(self, space: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -337,17 +210,17 @@ class Hyperopt(Backtesting):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def hyperopt_space(self) -> Dict[str, Any]:
|
def hyperopt_space(self) -> List[Dimension]:
|
||||||
"""
|
"""
|
||||||
Return the space to use during Hyperopt
|
Return the space to use during Hyperopt
|
||||||
"""
|
"""
|
||||||
spaces: Dict = {}
|
spaces: List[Dimension] = []
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
spaces = {**spaces, **Hyperopt.indicator_space()}
|
spaces += Hyperopt.indicator_space()
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
spaces = {**spaces, **Hyperopt.roi_space()}
|
spaces += Hyperopt.roi_space()
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
spaces = {**spaces, **Hyperopt.stoploss_space()}
|
spaces += Hyperopt.stoploss_space()
|
||||||
return spaces
|
return spaces
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -355,69 +228,32 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Define the buy strategy parameters to be used by hyperopt
|
Define the buy strategy parameters to be used by hyperopt
|
||||||
"""
|
"""
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Buy strategy Hyperopt will build and use
|
Buy strategy Hyperopt will build and use
|
||||||
"""
|
"""
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']:
|
if 'mfi-enabled' in params and params['mfi-enabled']:
|
||||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
conditions.append(dataframe['mfi'] < params['mfi-value'])
|
||||||
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']:
|
if 'fastd-enabled' in params and params['fastd-enabled']:
|
||||||
conditions.append(dataframe['macd'] < 0)
|
conditions.append(dataframe['fastd'] < params['fastd-value'])
|
||||||
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']:
|
if 'adx-enabled' in params and params['adx-enabled']:
|
||||||
conditions.append(dataframe['ema5'] > dataframe['ema10'])
|
conditions.append(dataframe['adx'] > params['adx-value'])
|
||||||
if 'mfi' in params and params['mfi']['enabled']:
|
if 'rsi-enabled' in params and params['rsi-enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
conditions.append(dataframe['rsi'] < params['rsi-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
|
||||||
triggers = {
|
if params['trigger'] == 'bb_lower':
|
||||||
'lower_bb': (
|
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
|
||||||
dataframe['close'] < dataframe['bb_lowerband']
|
if params['trigger'] == 'macd_cross_signal':
|
||||||
),
|
conditions.append(qtpylib.crossed_above(
|
||||||
'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']
|
dataframe['macd'], dataframe['macdsignal']
|
||||||
)),
|
))
|
||||||
'sar_reversal': (qtpylib.crossed_above(
|
if params['trigger'] == 'sar_reversal':
|
||||||
|
conditions.append(qtpylib.crossed_above(
|
||||||
dataframe['close'], dataframe['sar']
|
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[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
@ -427,21 +263,24 @@ class Hyperopt(Backtesting):
|
|||||||
|
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
def generate_optimizer(self, params: Dict) -> Dict:
|
def generate_optimizer(self, _params) -> Dict:
|
||||||
|
params = self.get_args(_params)
|
||||||
|
|
||||||
if self.has_space('roi'):
|
if self.has_space('roi'):
|
||||||
self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
|
self.strategy.minimal_roi = self.generate_roi_table(params)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.populate_buy_trend = self.buy_strategy_generator(params)
|
self.advise_buy = self.buy_strategy_generator(params)
|
||||||
|
|
||||||
if self.has_space('stoploss'):
|
if self.has_space('stoploss'):
|
||||||
self.analyze.strategy.stoploss = params['stoploss']
|
self.strategy.stoploss = params['stoploss']
|
||||||
|
|
||||||
|
processed = load(TICKERDATA_PICKLE)
|
||||||
results = self.backtest(
|
results = self.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': self.config['stake_amount'],
|
'stake_amount': self.config['stake_amount'],
|
||||||
'processed': self.processed,
|
'processed': processed,
|
||||||
'realistic': self.config.get('realistic_simulation', False),
|
'position_stacking': self.config.get('position_stacking', True),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result_explanation = self.format_results(results)
|
result_explanation = self.format_results(results)
|
||||||
@ -450,30 +289,18 @@ class Hyperopt(Backtesting):
|
|||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
trade_duration = results.trade_duration.mean()
|
trade_duration = results.trade_duration.mean()
|
||||||
|
|
||||||
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
|
if trade_count == 0:
|
||||||
print('.', end='')
|
|
||||||
sys.stdout.flush()
|
|
||||||
return {
|
return {
|
||||||
'status': STATUS_FAIL,
|
'loss': MAX_LOSS,
|
||||||
'loss': float('inf')
|
'params': params,
|
||||||
|
'result': result_explanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
|
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,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'loss': loss,
|
'loss': loss,
|
||||||
'status': STATUS_OK,
|
'params': params,
|
||||||
'result': result_explanation,
|
'result': result_explanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,15 +308,37 @@ class Hyperopt(Backtesting):
|
|||||||
"""
|
"""
|
||||||
Return the format result in a string
|
Return the format result in a string
|
||||||
"""
|
"""
|
||||||
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
trades = len(results.index)
|
||||||
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
|
avg_profit = results.profit_percent.mean() * 100.0
|
||||||
len(results.index),
|
total_profit = results.profit_abs.sum()
|
||||||
results.profit_percent.mean() * 100.0,
|
stake_cur = self.config['stake_currency']
|
||||||
results.profit_abs.sum(),
|
profit = results.profit_percent.sum()
|
||||||
self.config['stake_currency'],
|
duration = results.trade_duration.mean()
|
||||||
results.profit_percent.sum(),
|
|
||||||
results.trade_duration.mean(),
|
return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
|
||||||
)
|
f'Total profit {total_profit: 11.8f} {stake_cur} '
|
||||||
|
f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.')
|
||||||
|
|
||||||
|
def get_optimizer(self, cpu_count) -> Optimizer:
|
||||||
|
return Optimizer(
|
||||||
|
self.hyperopt_space(),
|
||||||
|
base_estimator="ET",
|
||||||
|
acq_optimizer="auto",
|
||||||
|
n_initial_points=30,
|
||||||
|
acq_optimizer_kwargs={'n_jobs': cpu_count}
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_optimizer_parallel(self, parallel, asked) -> List:
|
||||||
|
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
|
||||||
|
|
||||||
|
def load_previous_results(self):
|
||||||
|
""" 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()
|
||||||
|
logger.info(
|
||||||
|
'Loaded %d previous evaluations from disk.',
|
||||||
|
len(self.trials)
|
||||||
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
timerange = Arguments.parse_timerange(None if self.config.get(
|
timerange = Arguments.parse_timerange(None if self.config.get(
|
||||||
@ -502,68 +351,36 @@ class Hyperopt(Backtesting):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.has_space('buy'):
|
if self.has_space('buy'):
|
||||||
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
|
self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
|
||||||
self.processed = self.tickerdata_to_dataframe(data)
|
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
|
||||||
|
self.exchange = None # type: ignore
|
||||||
|
self.load_previous_results()
|
||||||
|
|
||||||
logger.info('Preparing Trials..')
|
cpus = multiprocessing.cpu_count()
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
opt = self.get_optimizer(cpus)
|
||||||
|
EVALS = max(self.total_tries // cpus, 1)
|
||||||
try:
|
try:
|
||||||
best_parameters = fmin(
|
with Parallel(n_jobs=cpus) as parallel:
|
||||||
fn=self.generate_optimizer,
|
for i in range(EVALS):
|
||||||
space=self.hyperopt_space(),
|
asked = opt.ask(n_points=cpus)
|
||||||
algo=tpe.suggest,
|
f_val = self.run_optimizer_parallel(parallel, asked)
|
||||||
max_evals=self.total_tries,
|
opt.tell(asked, [i['loss'] for i in f_val])
|
||||||
trials=self.trials
|
|
||||||
)
|
|
||||||
|
|
||||||
results = sorted(self.trials.results, key=itemgetter('loss'))
|
self.trials += f_val
|
||||||
best_result = results[0]['result']
|
for j in range(cpus):
|
||||||
|
self.log_results({
|
||||||
except ValueError:
|
'loss': f_val[j]['loss'],
|
||||||
best_parameters = {}
|
'current_tries': i * cpus + j,
|
||||||
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
|
'total_tries': self.total_tries,
|
||||||
'try with more epochs (param: -e).'
|
'result': f_val[j]['result'],
|
||||||
|
})
|
||||||
# Improve best parameter logging display
|
except KeyboardInterrupt:
|
||||||
if best_parameters:
|
print('User interrupted..')
|
||||||
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.save_trials()
|
||||||
self.log_trials_result()
|
self.log_trials_result()
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
def start(args: Namespace) -> None:
|
def start(args: Namespace) -> None:
|
||||||
|
@ -5,12 +5,11 @@ This module contains the class to persist trades into SQLite
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal, getcontext
|
from decimal import Decimal, getcontext
|
||||||
from typing import Dict, Optional, Any
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||||
create_engine)
|
create_engine, inspect)
|
||||||
from sqlalchemy import inspect
|
|
||||||
from sqlalchemy.exc import NoSuchModuleError
|
from sqlalchemy.exc import NoSuchModuleError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
@ -22,6 +21,7 @@ from freqtrade import OperationalException
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_DECL_BASE: Any = declarative_base()
|
_DECL_BASE: Any = declarative_base()
|
||||||
|
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
||||||
|
|
||||||
|
|
||||||
def init(config: Dict) -> None:
|
def init(config: Dict) -> None:
|
||||||
@ -46,10 +46,8 @@ def init(config: Dict) -> None:
|
|||||||
try:
|
try:
|
||||||
engine = create_engine(db_url, **kwargs)
|
engine = create_engine(db_url, **kwargs)
|
||||||
except NoSuchModuleError:
|
except NoSuchModuleError:
|
||||||
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
|
raise OperationalException(f'Given value for db_url: \'{db_url}\' '
|
||||||
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
|
f'is no valid database URL! (See {_SQL_DOCS_URL})')
|
||||||
)
|
|
||||||
raise OperationalException(error)
|
|
||||||
|
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||||
Trade.session = session()
|
Trade.session = session()
|
||||||
@ -66,6 +64,10 @@ def has_column(columns, searchname: str) -> bool:
|
|||||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_column_def(columns, column: str, default: str) -> str:
|
||||||
|
return default if not has_column(columns, column) else column
|
||||||
|
|
||||||
|
|
||||||
def check_migrate(engine) -> None:
|
def check_migrate(engine) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if migration is necessary and migrates if necessary
|
Checks if migration is necessary and migrates if necessary
|
||||||
@ -73,18 +75,38 @@ def check_migrate(engine) -> None:
|
|||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
|
|
||||||
cols = inspector.get_columns('trades')
|
cols = inspector.get_columns('trades')
|
||||||
|
tabs = inspector.get_table_names()
|
||||||
|
table_back_name = 'trades_bak'
|
||||||
|
for i, table_back_name in enumerate(tabs):
|
||||||
|
table_back_name = f'trades_bak{i}'
|
||||||
|
logger.info(f'trying {table_back_name}')
|
||||||
|
|
||||||
|
# Check for latest column
|
||||||
|
if not has_column(cols, 'ticker_interval'):
|
||||||
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
|
fee_close = get_column_def(cols, 'fee_close', 'fee')
|
||||||
|
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
|
||||||
|
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
|
||||||
|
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||||
|
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||||
|
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||||
|
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||||
|
strategy = get_column_def(cols, 'strategy', 'null')
|
||||||
|
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
|
||||||
|
|
||||||
if not has_column(cols, 'fee_open'):
|
|
||||||
# Schema migration necessary
|
# Schema migration necessary
|
||||||
engine.execute("alter table trades rename to trades_bak")
|
engine.execute(f"alter table trades rename to {table_back_name}")
|
||||||
# let SQLAlchemy create the schema as required
|
# let SQLAlchemy create the schema as required
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
|
|
||||||
# Copy data back - following the correct schema
|
# Copy data back - following the correct schema
|
||||||
engine.execute("""insert into trades
|
engine.execute(f"""insert into trades
|
||||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id)
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
|
stop_loss, initial_stop_loss, max_rate, sell_reason, strategy,
|
||||||
|
ticker_interval
|
||||||
|
)
|
||||||
select id, lower(exchange),
|
select id, lower(exchange),
|
||||||
case
|
case
|
||||||
when instr(pair, '_') != 0 then
|
when instr(pair, '_') != 0 then
|
||||||
@ -93,22 +115,20 @@ def check_migrate(engine) -> None:
|
|||||||
else pair
|
else pair
|
||||||
end
|
end
|
||||||
pair,
|
pair,
|
||||||
is_open, fee fee_open, fee fee_close,
|
is_open, {fee_open} fee_open, {fee_close} fee_close,
|
||||||
open_rate, null open_rate_requested, close_rate,
|
open_rate, {open_rate_requested} open_rate_requested, close_rate,
|
||||||
null close_rate_requested, close_profit,
|
{close_rate_requested} close_rate_requested, close_profit,
|
||||||
stake_amount, amount, open_date, close_date, open_order_id
|
stake_amount, amount, open_date, close_date, open_order_id,
|
||||||
from trades_bak
|
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||||
|
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
|
||||||
|
{ticker_interval} ticker_interval
|
||||||
|
from {table_back_name}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Reread columns - the above recreated the table!
|
# Reread columns - the above recreated the table!
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
cols = inspector.get_columns('trades')
|
cols = inspector.get_columns('trades')
|
||||||
|
|
||||||
if not has_column(cols, 'open_rate_requested'):
|
|
||||||
engine.execute("alter table trades add open_rate_requested float")
|
|
||||||
if not has_column(cols, 'close_rate_requested'):
|
|
||||||
engine.execute("alter table trades add close_rate_requested float")
|
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
"""
|
"""
|
||||||
@ -137,8 +157,8 @@ class Trade(_DECL_BASE):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
exchange = Column(String, nullable=False)
|
exchange = Column(String, nullable=False)
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False, index=True)
|
||||||
is_open = Column(Boolean, nullable=False, default=True)
|
is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||||
fee_open = Column(Float, nullable=False, default=0.0)
|
fee_open = Column(Float, nullable=False, default=0.0)
|
||||||
fee_close = Column(Float, nullable=False, default=0.0)
|
fee_close = Column(Float, nullable=False, default=0.0)
|
||||||
open_rate = Column(Float)
|
open_rate = Column(Float)
|
||||||
@ -151,15 +171,60 @@ class Trade(_DECL_BASE):
|
|||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
close_date = Column(DateTime)
|
close_date = Column(DateTime)
|
||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
# absolute value of the stop loss
|
||||||
|
stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the initial stop loss
|
||||||
|
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||||
|
# absolute value of the highest reached price
|
||||||
|
max_rate = Column(Float, nullable=True, default=0.0)
|
||||||
|
sell_reason = Column(String, nullable=True)
|
||||||
|
strategy = Column(String, nullable=True)
|
||||||
|
ticker_interval = Column(Integer, nullable=True)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||||
self.id,
|
|
||||||
self.pair,
|
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
|
||||||
self.amount,
|
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
|
||||||
self.open_rate,
|
|
||||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
||||||
)
|
"""this adjusts the stop loss to it's most recently observed setting"""
|
||||||
|
|
||||||
|
if initial and not (self.stop_loss is None or self.stop_loss == 0):
|
||||||
|
# Don't modify if called with initial and nothing to do
|
||||||
|
return
|
||||||
|
|
||||||
|
new_loss = float(current_price * (1 - abs(stoploss)))
|
||||||
|
|
||||||
|
# keeping track of the highest observed rate for this trade
|
||||||
|
if self.max_rate is None:
|
||||||
|
self.max_rate = current_price
|
||||||
|
else:
|
||||||
|
if current_price > self.max_rate:
|
||||||
|
self.max_rate = current_price
|
||||||
|
|
||||||
|
# no stop loss assigned yet
|
||||||
|
if not self.stop_loss:
|
||||||
|
logger.debug("assigning new stop loss")
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
self.initial_stop_loss = new_loss
|
||||||
|
|
||||||
|
# evaluate if the stop loss needs to be updated
|
||||||
|
else:
|
||||||
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
logger.debug("adjusted stop loss")
|
||||||
|
else:
|
||||||
|
logger.debug("keeping current stop loss")
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"{self.pair} - current price {current_price:.8f}, "
|
||||||
|
f"bought at {self.open_rate:.8f} and calculated "
|
||||||
|
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
|
||||||
|
f"stop at {self.stop_loss:.8f}. "
|
||||||
|
f"trailing stop loss saved us: "
|
||||||
|
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
|
||||||
|
f"and max observed rate was {self.max_rate:.8f}")
|
||||||
|
|
||||||
def update(self, order: Dict) -> None:
|
def update(self, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
@ -167,6 +232,7 @@ class Trade(_DECL_BASE):
|
|||||||
:param order: order retrieved by exchange.get_order()
|
:param order: order retrieved by exchange.get_order()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
order_type = order['type']
|
||||||
# Ignore open and cancelled orders
|
# Ignore open and cancelled orders
|
||||||
if order['status'] == 'open' or order['price'] is None:
|
if order['status'] == 'open' or order['price'] is None:
|
||||||
return
|
return
|
||||||
@ -174,16 +240,16 @@ class Trade(_DECL_BASE):
|
|||||||
logger.info('Updating trade (id=%d) ...', self.id)
|
logger.info('Updating trade (id=%d) ...', self.id)
|
||||||
|
|
||||||
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
getcontext().prec = 8 # Bittrex do not go above 8 decimal
|
||||||
if order['type'] == 'limit' and order['side'] == 'buy':
|
if order_type == 'limit' and order['side'] == 'buy':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = Decimal(order['price'])
|
self.open_rate = Decimal(order['price'])
|
||||||
self.amount = Decimal(order['amount'])
|
self.amount = Decimal(order['amount'])
|
||||||
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
elif order['type'] == 'limit' and order['side'] == 'sell':
|
elif order_type == 'limit' and order['side'] == 'sell':
|
||||||
self.close(order['price'])
|
self.close(order['price'])
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
raise ValueError(f'Unknown order type: {order_type}')
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
def close(self, rate: float) -> None:
|
def close(self, rate: float) -> None:
|
||||||
@ -254,7 +320,8 @@ class Trade(_DECL_BASE):
|
|||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
fee=(fee or self.fee_close)
|
||||||
)
|
)
|
||||||
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
profit = close_trade_price - open_trade_price
|
||||||
|
return float(f"{profit:.8f}")
|
||||||
|
|
||||||
def calc_profit_percent(
|
def calc_profit_percent(
|
||||||
self,
|
self,
|
||||||
@ -274,5 +341,5 @@ class Trade(_DECL_BASE):
|
|||||||
rate=(rate or self.close_rate),
|
rate=(rate or self.close_rate),
|
||||||
fee=(fee or self.fee_close)
|
fee=(fee or self.fee_close)
|
||||||
)
|
)
|
||||||
|
profit_percent = (close_trade_price / open_trade_price) - 1
|
||||||
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
return float(f"{profit_percent:.8f}")
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
from .rpc import RPC, RPCMessageType, RPCException # noqa
|
||||||
|
from .rpc_manager import RPCManager # noqa
|
@ -3,22 +3,36 @@ This module contains class to define a RPC communications
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import timedelta, datetime, date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Dict, Tuple, Any, List
|
from enum import Enum
|
||||||
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
import sqlalchemy as sql
|
||||||
from numpy import mean, nan_to_num
|
from numpy import mean, nan_to_num
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RPCMessageType(Enum):
|
||||||
|
STATUS_NOTIFICATION = 'status'
|
||||||
|
WARNING_NOTIFICATION = 'warning'
|
||||||
|
CUSTOM_NOTIFICATION = 'custom'
|
||||||
|
BUY_NOTIFICATION = 'buy'
|
||||||
|
SELL_NOTIFICATION = 'sell'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class RPCException(Exception):
|
class RPCException(Exception):
|
||||||
"""
|
"""
|
||||||
Should be raised with a rpc-formatted message in an _rpc_* method
|
Should be raised with a rpc-formatted message in an _rpc_* method
|
||||||
@ -26,13 +40,21 @@ class RPCException(Exception):
|
|||||||
|
|
||||||
raise RPCException('*Status:* `no active trade`')
|
raise RPCException('*Status:* `no active trade`')
|
||||||
"""
|
"""
|
||||||
pass
|
def __init__(self, message: str) -> None:
|
||||||
|
super().__init__(self)
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class RPC(object):
|
class RPC(object):
|
||||||
"""
|
"""
|
||||||
RPC class can be used to have extra feature, like bot data, and access to DB data
|
RPC class can be used to have extra feature, like bot data, and access to DB data
|
||||||
"""
|
"""
|
||||||
|
# Bind _fiat_converter if needed in each RPC handler
|
||||||
|
_fiat_converter: Optional[CryptoToFiatConverter] = None
|
||||||
|
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes all enabled rpc modules
|
Initializes all enabled rpc modules
|
||||||
@ -41,20 +63,20 @@ class RPC(object):
|
|||||||
"""
|
"""
|
||||||
self._freqtrade = freqtrade
|
self._freqtrade = freqtrade
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
""" Returns the lowercase name of the implementation """
|
||||||
|
return self.__class__.__name__.lower()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
""" Cleanup pending module resources """
|
""" Cleanup pending module resources """
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def name(self) -> str:
|
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||||
""" Returns the lowercase name of this module """
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def send_msg(self, msg: str) -> None:
|
|
||||||
""" Sends a message to all registered rpc modules """
|
""" Sends a message to all registered rpc modules """
|
||||||
|
|
||||||
def _rpc_trade_status(self) -> List[str]:
|
def _rpc_trade_status(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
|
||||||
a remotely exposed function
|
a remotely exposed function
|
||||||
@ -62,11 +84,11 @@ class RPC(object):
|
|||||||
# Fetch open trade
|
# Fetch open trade
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('*Status:* `trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
raise RPCException('*Status:* `no active trade`')
|
raise RPCException('no active trade')
|
||||||
else:
|
else:
|
||||||
result = []
|
results = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
order = None
|
order = None
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
@ -74,53 +96,42 @@ class RPC(object):
|
|||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
current_profit = trade.calc_profit_percent(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
fmt_close_profit = '{:.2f}%'.format(
|
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
|
||||||
round(trade.close_profit * 100, 2)
|
if trade.close_profit else None)
|
||||||
) if trade.close_profit else None
|
results.append(dict(
|
||||||
message = "*Trade ID:* `{trade_id}`\n" \
|
trade_id=trade.id,
|
||||||
"*Current Pair:* [{pair}]({market_url})\n" \
|
pair=trade.pair,
|
||||||
"*Open Since:* `{date}`\n" \
|
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||||
"*Amount:* `{amount}`\n" \
|
date=arrow.get(trade.open_date),
|
||||||
"*Open Rate:* `{open_rate:.8f}`\n" \
|
open_rate=trade.open_rate,
|
||||||
"*Close Rate:* `{close_rate}`\n" \
|
close_rate=trade.close_rate,
|
||||||
"*Current Rate:* `{current_rate:.8f}`\n" \
|
current_rate=current_rate,
|
||||||
"*Close Profit:* `{close_profit}`\n" \
|
amount=round(trade.amount, 8),
|
||||||
"*Current Profit:* `{current_profit:.2f}%`\n" \
|
close_profit=fmt_close_profit,
|
||||||
"*Open Order:* `{open_order}`"\
|
current_profit=round(current_profit * 100, 2),
|
||||||
.format(
|
open_order='({} {} rem={:.8f})'.format(
|
||||||
trade_id=trade.id,
|
order['type'], order['side'], order['remaining']
|
||||||
pair=trade.pair,
|
) if order else None,
|
||||||
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
))
|
||||||
date=arrow.get(trade.open_date).humanize(),
|
return results
|
||||||
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['side'], order['remaining']
|
|
||||||
) if order else None,
|
|
||||||
)
|
|
||||||
result.append(message)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _rpc_status_table(self) -> DataFrame:
|
def _rpc_status_table(self) -> DataFrame:
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('*Status:* `trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
elif not trades:
|
elif not trades:
|
||||||
raise RPCException('*Status:* `no active order`')
|
raise RPCException('no active order')
|
||||||
else:
|
else:
|
||||||
trades_list = []
|
trades_list = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
|
trade_perc = (100 * trade.calc_profit_percent(current_rate))
|
||||||
trades_list.append([
|
trades_list.append([
|
||||||
trade.id,
|
trade.id,
|
||||||
trade.pair,
|
trade.pair,
|
||||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||||
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
f'{trade_perc:.2f}%'
|
||||||
])
|
])
|
||||||
|
|
||||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||||
@ -135,9 +146,8 @@ class RPC(object):
|
|||||||
profit_days: Dict[date, Dict] = {}
|
profit_days: Dict[date, Dict] = {}
|
||||||
|
|
||||||
if not (isinstance(timescale, int) and timescale > 0):
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
|
raise RPCException('timescale must be an integer greater than 0')
|
||||||
|
|
||||||
fiat = self._freqtrade.fiat_converter
|
|
||||||
for day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = today - timedelta(days=day)
|
||||||
trades = Trade.query \
|
trades = Trade.query \
|
||||||
@ -148,7 +158,7 @@ class RPC(object):
|
|||||||
.all()
|
.all()
|
||||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||||
profit_days[profitday] = {
|
profit_days[profitday] = {
|
||||||
'amount': format(curdayprofit, '.8f'),
|
'amount': f'{curdayprofit:.8f}',
|
||||||
'trades': len(trades)
|
'trades': len(trades)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,11 +170,11 @@ class RPC(object):
|
|||||||
symbol=stake_currency
|
symbol=stake_currency
|
||||||
),
|
),
|
||||||
'{value:.3f} {symbol}'.format(
|
'{value:.3f} {symbol}'.format(
|
||||||
value=fiat.convert_amount(
|
value=self._fiat_converter.convert_amount(
|
||||||
value['amount'],
|
value['amount'],
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
),
|
) if self._fiat_converter else 0,
|
||||||
symbol=fiat_display_currency
|
symbol=fiat_display_currency
|
||||||
),
|
),
|
||||||
'{value} trade{s}'.format(
|
'{value} trade{s}'.format(
|
||||||
@ -215,34 +225,33 @@ class RPC(object):
|
|||||||
.order_by(sql.text('profit_sum DESC')).first()
|
.order_by(sql.text('profit_sum DESC')).first()
|
||||||
|
|
||||||
if not best_pair:
|
if not best_pair:
|
||||||
raise RPCException('*Status:* `no closed trade`')
|
raise RPCException('no closed trade')
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
# FIX: we want to keep fiatconverter in a state/environment,
|
|
||||||
# doing this will utilize its caching functionallity, instead we reinitialize it here
|
|
||||||
fiat = self._freqtrade.fiat_converter
|
|
||||||
# Prepare data to display
|
# Prepare data to display
|
||||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
|
||||||
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
|
||||||
profit_closed_fiat = fiat.convert_amount(
|
profit_closed_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_closed_coin,
|
profit_closed_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
) if self._fiat_converter else 0
|
||||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
|
||||||
|
profit_all_coin_sum = round(sum(profit_all_coin), 8)
|
||||||
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
|
||||||
profit_all_fiat = fiat.convert_amount(
|
profit_all_fiat = self._fiat_converter.convert_amount(
|
||||||
profit_all_coin,
|
profit_all_coin_sum,
|
||||||
stake_currency,
|
stake_currency,
|
||||||
fiat_display_currency
|
fiat_display_currency
|
||||||
)
|
) if self._fiat_converter else 0
|
||||||
|
|
||||||
num = float(len(durations) or 1)
|
num = float(len(durations) or 1)
|
||||||
return {
|
return {
|
||||||
'profit_closed_coin': profit_closed_coin,
|
'profit_closed_coin': profit_closed_coin_sum,
|
||||||
'profit_closed_percent': profit_closed_percent,
|
'profit_closed_percent': profit_closed_percent,
|
||||||
'profit_closed_fiat': profit_closed_fiat,
|
'profit_closed_fiat': profit_closed_fiat,
|
||||||
'profit_all_coin': profit_all_coin,
|
'profit_all_coin': profit_all_coin_sum,
|
||||||
'profit_all_percent': profit_all_percent,
|
'profit_all_percent': profit_all_percent,
|
||||||
'profit_all_fiat': profit_all_fiat,
|
'profit_all_fiat': profit_all_fiat,
|
||||||
'trade_count': len(trades),
|
'trade_count': len(trades),
|
||||||
@ -253,7 +262,7 @@ class RPC(object):
|
|||||||
'best_rate': round(bp_rate * 100, 2),
|
'best_rate': round(bp_rate * 100, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
|
def _rpc_balance(self, fiat_display_currency: str) -> Dict:
|
||||||
""" Returns current account balance per crypto """
|
""" Returns current account balance per crypto """
|
||||||
output = []
|
output = []
|
||||||
total = 0.0
|
total = 0.0
|
||||||
@ -270,45 +279,47 @@ class RPC(object):
|
|||||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||||
est_btc: float = rate * balance['total']
|
est_btc: float = rate * balance['total']
|
||||||
total = total + est_btc
|
total = total + est_btc
|
||||||
output.append(
|
output.append({
|
||||||
{
|
'currency': coin,
|
||||||
'currency': coin,
|
'available': balance['free'],
|
||||||
'available': balance['free'],
|
'balance': balance['total'],
|
||||||
'balance': balance['total'],
|
'pending': balance['used'],
|
||||||
'pending': balance['used'],
|
'est_btc': est_btc,
|
||||||
'est_btc': est_btc
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
if total == 0.0:
|
if total == 0.0:
|
||||||
raise RPCException('`All balances are zero.`')
|
raise RPCException('all balances are zero')
|
||||||
|
|
||||||
fiat = self._freqtrade.fiat_converter
|
|
||||||
symbol = fiat_display_currency
|
symbol = fiat_display_currency
|
||||||
value = fiat.convert_amount(total, 'BTC', symbol)
|
value = self._fiat_converter.convert_amount(total, 'BTC',
|
||||||
return output, total, symbol, value
|
symbol) if self._fiat_converter else 0
|
||||||
|
return {
|
||||||
|
'currencies': output,
|
||||||
|
'total': total,
|
||||||
|
'symbol': symbol,
|
||||||
|
'value': value,
|
||||||
|
}
|
||||||
|
|
||||||
def _rpc_start(self) -> str:
|
def _rpc_start(self) -> Dict[str, str]:
|
||||||
""" Handler for start """
|
""" Handler for start """
|
||||||
if self._freqtrade.state == State.RUNNING:
|
if self._freqtrade.state == State.RUNNING:
|
||||||
return '*Status:* `already running`'
|
return {'status': 'already running'}
|
||||||
|
|
||||||
self._freqtrade.state = State.RUNNING
|
self._freqtrade.state = State.RUNNING
|
||||||
return '`Starting trader ...`'
|
return {'status': 'starting trader ...'}
|
||||||
|
|
||||||
def _rpc_stop(self) -> str:
|
def _rpc_stop(self) -> Dict[str, str]:
|
||||||
""" Handler for stop """
|
""" Handler for stop """
|
||||||
if self._freqtrade.state == State.RUNNING:
|
if self._freqtrade.state == State.RUNNING:
|
||||||
self._freqtrade.state = State.STOPPED
|
self._freqtrade.state = State.STOPPED
|
||||||
return '`Stopping trader ...`'
|
return {'status': 'stopping trader ...'}
|
||||||
|
|
||||||
return '*Status:* `already stopped`'
|
return {'status': 'already stopped'}
|
||||||
|
|
||||||
def _rpc_reload_conf(self) -> str:
|
def _rpc_reload_conf(self) -> Dict[str, str]:
|
||||||
""" Handler for reload_conf. """
|
""" Handler for reload_conf. """
|
||||||
self._freqtrade.state = State.RELOAD_CONF
|
self._freqtrade.state = State.RELOAD_CONF
|
||||||
return '*Status:* `Reloading config ...`'
|
return {'status': 'reloading config ...'}
|
||||||
|
|
||||||
# FIX: no test for this!!!!
|
|
||||||
def _rpc_forcesell(self, trade_id) -> None:
|
def _rpc_forcesell(self, trade_id) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
@ -338,11 +349,11 @@ class RPC(object):
|
|||||||
|
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
|
||||||
self._freqtrade.execute_sell(trade, current_rate)
|
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('`trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
@ -359,7 +370,7 @@ class RPC(object):
|
|||||||
).first()
|
).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
logger.warning('forcesell: Invalid argument received')
|
logger.warning('forcesell: Invalid argument received')
|
||||||
raise RPCException('Invalid argument.')
|
raise RPCException('invalid argument')
|
||||||
|
|
||||||
_exec_forcesell(trade)
|
_exec_forcesell(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
@ -370,7 +381,7 @@ class RPC(object):
|
|||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('`trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
pair_rates = Trade.session.query(Trade.pair,
|
pair_rates = Trade.session.query(Trade.pair,
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
@ -387,6 +398,6 @@ class RPC(object):
|
|||||||
def _rpc_count(self) -> List[Trade]:
|
def _rpc_count(self) -> List[Trade]:
|
||||||
""" Returns the number of trades running """
|
""" Returns the number of trades running """
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('`trader is not running`')
|
raise RPCException('trader is not running')
|
||||||
|
|
||||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
This module contains class to manage RPC communications (Telegram, Slack, ...)
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
from freqtrade.rpc.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -23,6 +23,12 @@ class RPCManager(object):
|
|||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc.telegram import Telegram
|
||||||
self.registered_modules.append(Telegram(freqtrade))
|
self.registered_modules.append(Telegram(freqtrade))
|
||||||
|
|
||||||
|
# Enable Webhook
|
||||||
|
if freqtrade.config.get('webhook', {}).get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.webhook ...')
|
||||||
|
from freqtrade.rpc.webhook import Webhook
|
||||||
|
self.registered_modules.append(Webhook(freqtrade))
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
""" Stops all enabled rpc modules """
|
""" Stops all enabled rpc modules """
|
||||||
logger.info('Cleaning up rpc modules ...')
|
logger.info('Cleaning up rpc modules ...')
|
||||||
@ -32,11 +38,14 @@ class RPCManager(object):
|
|||||||
mod.cleanup()
|
mod.cleanup()
|
||||||
del mod
|
del mod
|
||||||
|
|
||||||
def send_msg(self, msg: str) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message to all registered rpc modules
|
Send given message to all registered rpc modules.
|
||||||
:param msg: message
|
A message consists of one or more key value pairs of strings.
|
||||||
:return: None
|
e.g.:
|
||||||
|
{
|
||||||
|
'status': 'stopping bot'
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
logger.info('Sending rpc message: %s', msg)
|
logger.info('Sending rpc message: %s', msg)
|
||||||
for mod in self.registered_modules:
|
for mod in self.registered_modules:
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
This module manage Telegram communication
|
This module manage Telegram communication
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
||||||
@ -12,7 +12,8 @@ from telegram.error import NetworkError, TelegramError
|
|||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
|
from freqtrade.rpc import RPC, RPCException, RPCMessageType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -55,10 +56,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
|
|||||||
class Telegram(RPC):
|
class Telegram(RPC):
|
||||||
""" This class handles all telegram communication """
|
""" This class handles all telegram communication """
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
return "telegram"
|
|
||||||
|
|
||||||
def __init__(self, freqtrade) -> None:
|
def __init__(self, freqtrade) -> None:
|
||||||
"""
|
"""
|
||||||
Init the Telegram call, and init the super class RPC
|
Init the Telegram call, and init the super class RPC
|
||||||
@ -70,6 +67,8 @@ class Telegram(RPC):
|
|||||||
self._updater: Updater = None
|
self._updater: Updater = None
|
||||||
self._config = freqtrade.config
|
self._config = freqtrade.config
|
||||||
self._init()
|
self._init()
|
||||||
|
if self._config.get('fiat_display_currency', None):
|
||||||
|
self._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
def _init(self) -> None:
|
def _init(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -114,9 +113,57 @@ class Telegram(RPC):
|
|||||||
"""
|
"""
|
||||||
self._updater.stop()
|
self._updater.stop()
|
||||||
|
|
||||||
def send_msg(self, msg: str) -> None:
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
""" Send a message to telegram channel """
|
""" Send a message to telegram channel """
|
||||||
self._send_msg(msg)
|
|
||||||
|
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||||
|
if self._fiat_converter:
|
||||||
|
msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
|
||||||
|
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
|
else:
|
||||||
|
msg['stake_amount_fiat'] = 0
|
||||||
|
|
||||||
|
message = "*{exchange}:* Buying [{pair}]({market_url})\n" \
|
||||||
|
"with limit `{limit:.8f}\n" \
|
||||||
|
"({stake_amount:.6f} {stake_currency}".format(**msg)
|
||||||
|
|
||||||
|
if msg.get('fiat_currency', None):
|
||||||
|
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||||
|
message += ")`"
|
||||||
|
|
||||||
|
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||||
|
msg['amount'] = round(msg['amount'], 8)
|
||||||
|
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||||
|
|
||||||
|
message = "*{exchange}:* Selling [{pair}]({market_url})\n" \
|
||||||
|
"*Limit:* `{limit:.8f}`\n" \
|
||||||
|
"*Amount:* `{amount:.8f}`\n" \
|
||||||
|
"*Open Rate:* `{open_rate:.8f}`\n" \
|
||||||
|
"*Current Rate:* `{current_rate:.8f}`\n" \
|
||||||
|
"*Profit:* `{profit_percent:.2f}%`".format(**msg)
|
||||||
|
|
||||||
|
# Check if all sell properties are available.
|
||||||
|
# This might not be the case if the message origin is triggered by /forcesell
|
||||||
|
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
|
||||||
|
and self._fiat_converter):
|
||||||
|
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||||
|
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||||
|
message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \
|
||||||
|
'` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||||
|
message = '*Status:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
|
||||||
|
message = '*Warning:* `{status}`'.format(**msg)
|
||||||
|
|
||||||
|
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
|
||||||
|
message = '{status}'.format(**msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||||
|
|
||||||
|
self._send_msg(message)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _status(self, bot: Bot, update: Update) -> None:
|
def _status(self, bot: Bot, update: Update) -> None:
|
||||||
@ -136,8 +183,26 @@ class Telegram(RPC):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for trade_msg in self._rpc_trade_status():
|
results = self._rpc_trade_status()
|
||||||
self._send_msg(trade_msg, bot=bot)
|
# pre format data
|
||||||
|
for result in results:
|
||||||
|
result['date'] = result['date'].humanize()
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
"*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(**result)
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
for msg in messages:
|
||||||
|
self._send_msg(msg, bot=bot)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@ -153,7 +218,7 @@ class Telegram(RPC):
|
|||||||
try:
|
try:
|
||||||
df_statuses = self._rpc_status_table()
|
df_statuses = self._rpc_status_table()
|
||||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||||
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
|
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
|
|
||||||
@ -166,6 +231,8 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
try:
|
try:
|
||||||
timescale = int(update.message.text.replace('/daily', '').strip())
|
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@ -173,18 +240,17 @@ class Telegram(RPC):
|
|||||||
try:
|
try:
|
||||||
stats = self._rpc_daily_profit(
|
stats = self._rpc_daily_profit(
|
||||||
timescale,
|
timescale,
|
||||||
self._config['stake_currency'],
|
stake_cur,
|
||||||
self._config['fiat_display_currency']
|
fiat_disp_cur
|
||||||
)
|
)
|
||||||
stats = tabulate(stats,
|
stats = tabulate(stats,
|
||||||
headers=[
|
headers=[
|
||||||
'Day',
|
'Day',
|
||||||
'Profit {}'.format(self._config['stake_currency']),
|
f'Profit {stake_cur}',
|
||||||
'Profit {}'.format(self._config['fiat_display_currency'])
|
f'Profit {fiat_disp_cur}'
|
||||||
],
|
],
|
||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
|
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
|
||||||
.format(timescale, stats)
|
|
||||||
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
@ -198,39 +264,38 @@ class Telegram(RPC):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats = self._rpc_trade_statistics(
|
stats = self._rpc_trade_statistics(
|
||||||
self._config['stake_currency'],
|
stake_cur,
|
||||||
self._config['fiat_display_currency'])
|
fiat_disp_cur)
|
||||||
|
profit_closed_coin = stats['profit_closed_coin']
|
||||||
|
profit_closed_percent = stats['profit_closed_percent']
|
||||||
|
profit_closed_fiat = stats['profit_closed_fiat']
|
||||||
|
profit_all_coin = stats['profit_all_coin']
|
||||||
|
profit_all_percent = stats['profit_all_percent']
|
||||||
|
profit_all_fiat = stats['profit_all_fiat']
|
||||||
|
trade_count = stats['trade_count']
|
||||||
|
first_trade_date = stats['first_trade_date']
|
||||||
|
latest_trade_date = stats['latest_trade_date']
|
||||||
|
avg_duration = stats['avg_duration']
|
||||||
|
best_pair = stats['best_pair']
|
||||||
|
best_rate = stats['best_rate']
|
||||||
# Message to display
|
# Message to display
|
||||||
markdown_msg = "*ROI:* Close trades\n" \
|
markdown_msg = "*ROI:* Close trades\n" \
|
||||||
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
|
f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
|
||||||
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
|
f"({profit_closed_percent:.2f}%)`\n" \
|
||||||
"*ROI:* All trades\n" \
|
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
|
||||||
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
|
f"*ROI:* All trades\n" \
|
||||||
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
|
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
|
||||||
"*Total Trade Count:* `{trade_count}`\n" \
|
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
|
||||||
"*First Trade opened:* `{first_trade_date}`\n" \
|
f"*Total Trade Count:* `{trade_count}`\n" \
|
||||||
"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
f"*First Trade opened:* `{first_trade_date}`\n" \
|
||||||
"*Avg. Duration:* `{avg_duration}`\n" \
|
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
|
||||||
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
|
f"*Avg. Duration:* `{avg_duration}`\n" \
|
||||||
.format(
|
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
|
||||||
coin=self._config['stake_currency'],
|
|
||||||
fiat=self._config['fiat_display_currency'],
|
|
||||||
profit_closed_coin=stats['profit_closed_coin'],
|
|
||||||
profit_closed_percent=stats['profit_closed_percent'],
|
|
||||||
profit_closed_fiat=stats['profit_closed_fiat'],
|
|
||||||
profit_all_coin=stats['profit_all_coin'],
|
|
||||||
profit_all_percent=stats['profit_all_percent'],
|
|
||||||
profit_all_fiat=stats['profit_all_fiat'],
|
|
||||||
trade_count=stats['trade_count'],
|
|
||||||
first_trade_date=stats['first_trade_date'],
|
|
||||||
latest_trade_date=stats['latest_trade_date'],
|
|
||||||
avg_duration=stats['avg_duration'],
|
|
||||||
best_pair=stats['best_pair'],
|
|
||||||
best_rate=stats['best_rate']
|
|
||||||
)
|
|
||||||
self._send_msg(markdown_msg, bot=bot)
|
self._send_msg(markdown_msg, bot=bot)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
@ -239,10 +304,9 @@ class Telegram(RPC):
|
|||||||
def _balance(self, bot: Bot, update: Update) -> None:
|
def _balance(self, bot: Bot, update: Update) -> None:
|
||||||
""" Handler for /balance """
|
""" Handler for /balance """
|
||||||
try:
|
try:
|
||||||
currencys, total, symbol, value = \
|
result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
|
||||||
self._rpc_balance(self._config['fiat_display_currency'])
|
|
||||||
output = ''
|
output = ''
|
||||||
for currency in currencys:
|
for currency in result['currencies']:
|
||||||
output += "*{currency}:*\n" \
|
output += "*{currency}:*\n" \
|
||||||
"\t`Available: {available: .8f}`\n" \
|
"\t`Available: {available: .8f}`\n" \
|
||||||
"\t`Balance: {balance: .8f}`\n" \
|
"\t`Balance: {balance: .8f}`\n" \
|
||||||
@ -250,8 +314,8 @@ class Telegram(RPC):
|
|||||||
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
|
||||||
|
|
||||||
output += "\n*Estimated Value*:\n" \
|
output += "\n*Estimated Value*:\n" \
|
||||||
"\t`BTC: {0: .8f}`\n" \
|
"\t`BTC: {total: .8f}`\n" \
|
||||||
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
|
"\t`{symbol}: {value: .2f}`\n".format(**result)
|
||||||
self._send_msg(output, bot=bot)
|
self._send_msg(output, bot=bot)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e), bot=bot)
|
self._send_msg(str(e), bot=bot)
|
||||||
@ -266,7 +330,7 @@ class Telegram(RPC):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_start()
|
msg = self._rpc_start()
|
||||||
self._send_msg(msg, bot=bot)
|
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _stop(self, bot: Bot, update: Update) -> None:
|
def _stop(self, bot: Bot, update: Update) -> None:
|
||||||
@ -278,7 +342,7 @@ class Telegram(RPC):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_stop()
|
msg = self._rpc_stop()
|
||||||
self._send_msg(msg, bot=bot)
|
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
def _reload_conf(self, bot: Bot, update: Update) -> None:
|
||||||
@ -290,7 +354,7 @@ class Telegram(RPC):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
msg = self._rpc_reload_conf()
|
msg = self._rpc_reload_conf()
|
||||||
self._send_msg(msg, bot=bot)
|
self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(self, bot: Bot, update: Update) -> None:
|
def _forcesell(self, bot: Bot, update: Update) -> None:
|
||||||
|
66
freqtrade/rpc/webhook.py
Normal file
66
freqtrade/rpc/webhook.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
This module manages webhook communication
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from requests import post, RequestException
|
||||||
|
|
||||||
|
from freqtrade.rpc import RPC, RPCMessageType
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.debug('Included module rpc.webhook ...')
|
||||||
|
|
||||||
|
|
||||||
|
class Webhook(RPC):
|
||||||
|
""" This class handles all webhook communication """
|
||||||
|
|
||||||
|
def __init__(self, freqtrade) -> None:
|
||||||
|
"""
|
||||||
|
Init the Webhook class, and init the super class RPC
|
||||||
|
:param freqtrade: Instance of a freqtrade bot
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
super().__init__(freqtrade)
|
||||||
|
|
||||||
|
self._config = freqtrade.config
|
||||||
|
self._url = self._config['webhook']['url']
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Cleanup pending module resources.
|
||||||
|
This will do nothing for webhooks, they will simply not be called anymore
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_msg(self, msg: Dict[str, Any]) -> None:
|
||||||
|
""" Send a message to telegram channel """
|
||||||
|
try:
|
||||||
|
|
||||||
|
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||||
|
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||||
|
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||||
|
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||||
|
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||||
|
valuedict = self._config['webhook'].get('webhookstatus', None)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
|
||||||
|
if not valuedict:
|
||||||
|
logger.info("Message type %s not configured for webhooks", msg['type'])
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
|
||||||
|
self._send_msg(payload)
|
||||||
|
except KeyError as exc:
|
||||||
|
logger.exception("Problem calling Webhook. Please check your webhook configuration. "
|
||||||
|
"Exception: %s", exc)
|
||||||
|
|
||||||
|
def _send_msg(self, payload: dict) -> None:
|
||||||
|
"""do the actual call to the webhook"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
post(self._url, data=payload)
|
||||||
|
except RequestException as exc:
|
||||||
|
logger.warning("Could not call webhook url. Exception: %s", exc)
|
@ -7,7 +7,7 @@ from freqtrade.strategy.interface import IStrategy
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def import_strategy(strategy: IStrategy) -> IStrategy:
|
def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Imports given Strategy instance to global scope
|
Imports given Strategy instance to global scope
|
||||||
of freqtrade.strategy and returns an instance of it
|
of freqtrade.strategy and returns an instance of it
|
||||||
@ -29,4 +29,4 @@ def import_strategy(strategy: IStrategy) -> IStrategy:
|
|||||||
# Modify global scope to declare class
|
# Modify global scope to declare class
|
||||||
globals()[name] = clazz
|
globals()[name] = clazz
|
||||||
|
|
||||||
return clazz()
|
return clazz(config)
|
||||||
|
@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
|
|||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = '5m'
|
ticker_interval = '5m'
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
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
|
you are using. Let uncomment only the indicator you are using in your strategies
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Momentum Indicator
|
# Momentum Indicator
|
||||||
@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> 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
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> 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
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
|
@ -2,11 +2,50 @@
|
|||||||
IStrategy interface
|
IStrategy interface
|
||||||
This module defines the interface to apply for strategies
|
This module defines the interface to apply for strategies
|
||||||
"""
|
"""
|
||||||
from typing import Dict
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, List, NamedTuple, Tuple
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import constants
|
||||||
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SignalType(Enum):
|
||||||
|
"""
|
||||||
|
Enum to distinguish between buy and sell signals
|
||||||
|
"""
|
||||||
|
BUY = "buy"
|
||||||
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
|
class SellType(Enum):
|
||||||
|
"""
|
||||||
|
Enum to distinguish between sell reasons
|
||||||
|
"""
|
||||||
|
ROI = "roi"
|
||||||
|
STOP_LOSS = "stop_loss"
|
||||||
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
|
SELL_SIGNAL = "sell_signal"
|
||||||
|
FORCE_SELL = "force_sell"
|
||||||
|
NONE = ""
|
||||||
|
|
||||||
|
|
||||||
|
class SellCheckTuple(NamedTuple):
|
||||||
|
"""
|
||||||
|
NamedTuple for Sell type + reason
|
||||||
|
"""
|
||||||
|
sell_flag: bool
|
||||||
|
sell_type: SellType
|
||||||
|
|
||||||
|
|
||||||
class IStrategy(ABC):
|
class IStrategy(ABC):
|
||||||
"""
|
"""
|
||||||
@ -19,30 +58,267 @@ class IStrategy(ABC):
|
|||||||
ticker_interval -> str: value of the ticker interval to use for the strategy
|
ticker_interval -> str: value of the ticker interval to use for the strategy
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_populate_fun_len: int = 0
|
||||||
|
_buy_fun_len: int = 0
|
||||||
|
_sell_fun_len: int = 0
|
||||||
|
# associated minimal roi
|
||||||
minimal_roi: Dict
|
minimal_roi: Dict
|
||||||
|
|
||||||
|
# associated stoploss
|
||||||
stoploss: float
|
stoploss: float
|
||||||
|
|
||||||
|
# associated ticker interval
|
||||||
ticker_interval: str
|
ticker_interval: str
|
||||||
|
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
self.config = config
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Populate indicators that will be used in the Buy and Sell strategy
|
Populate indicators that will be used in the Buy and Sell strategy
|
||||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> 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
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> 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
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with sell column
|
:return: DataFrame with sell column
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_strategy_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Returns strategy class name
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
def analyze_ticker(self, ticker_history: List[Dict], metadata: 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 = self.advise_indicators(dataframe, metadata)
|
||||||
|
dataframe = self.advise_buy(dataframe, metadata)
|
||||||
|
dataframe = self.advise_sell(dataframe, metadata)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]:
|
||||||
|
"""
|
||||||
|
Calculates current signal based several technical analysis indicators
|
||||||
|
:param pair: pair in format ANT/BTC
|
||||||
|
:param interval: Interval to use (in min)
|
||||||
|
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||||
|
"""
|
||||||
|
if not ticker_hist:
|
||||||
|
logger.warning('Empty ticker history for pair %s', pair)
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
|
||||||
|
except ValueError as error:
|
||||||
|
logger.warning(
|
||||||
|
'Unable to analyze ticker for pair %s: %s',
|
||||||
|
pair,
|
||||||
|
str(error)
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
except Exception as error:
|
||||||
|
logger.exception(
|
||||||
|
'Unexpected error when analyzing ticker for pair %s: %s',
|
||||||
|
pair,
|
||||||
|
str(error)
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
if dataframe.empty:
|
||||||
|
logger.warning('Empty dataframe for pair %s', pair)
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
latest = dataframe.iloc[-1]
|
||||||
|
|
||||||
|
# Check if dataframe is out of date
|
||||||
|
signal_date = arrow.get(latest['date'])
|
||||||
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
|
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
||||||
|
logger.warning(
|
||||||
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
|
pair,
|
||||||
|
(arrow.utcnow() - signal_date).seconds // 60
|
||||||
|
)
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||||
|
logger.debug(
|
||||||
|
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||||
|
latest['date'],
|
||||||
|
pair,
|
||||||
|
str(buy),
|
||||||
|
str(sell)
|
||||||
|
)
|
||||||
|
return buy, sell
|
||||||
|
|
||||||
|
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
||||||
|
sell: bool) -> SellCheckTuple:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
current_profit = trade.calc_profit_percent(rate)
|
||||||
|
stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
|
||||||
|
current_profit=current_profit)
|
||||||
|
if stoplossflag.sell_flag:
|
||||||
|
return stoplossflag
|
||||||
|
|
||||||
|
experimental = self.config.get('experimental', {})
|
||||||
|
|
||||||
|
if buy and experimental.get('ignore_roi_if_buy_signal', False):
|
||||||
|
logger.debug('Buy signal still active - not selling.')
|
||||||
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
|
||||||
|
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
|
||||||
|
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
|
||||||
|
logger.debug('Required profit reached. Selling..')
|
||||||
|
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
||||||
|
|
||||||
|
if experimental.get('sell_profit_only', False):
|
||||||
|
logger.debug('Checking if trade is profitable..')
|
||||||
|
if trade.calc_profit(rate=rate) <= 0:
|
||||||
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
if sell and not buy and experimental.get('use_sell_signal', False):
|
||||||
|
logger.debug('Sell signal received. Selling..')
|
||||||
|
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
||||||
|
|
||||||
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
|
||||||
|
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
|
||||||
|
current_profit: float) -> SellCheckTuple:
|
||||||
|
"""
|
||||||
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
|
decides to sell or not
|
||||||
|
:param current_profit: current profit in percent
|
||||||
|
"""
|
||||||
|
|
||||||
|
trailing_stop = self.config.get('trailing_stop', False)
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, self.stoploss, initial=True)
|
||||||
|
|
||||||
|
# evaluate if the stoploss was hit
|
||||||
|
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
||||||
|
selltype = SellType.STOP_LOSS
|
||||||
|
if trailing_stop:
|
||||||
|
selltype = SellType.TRAILING_STOP_LOSS
|
||||||
|
logger.debug(
|
||||||
|
f"HIT STOP: current price at {current_rate:.6f}, "
|
||||||
|
f"stop loss is {trade.stop_loss:.6f}, "
|
||||||
|
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
|
||||||
|
f"trade opened at {trade.open_rate:.6f}")
|
||||||
|
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||||
|
|
||||||
|
logger.debug('Stop loss hit.')
|
||||||
|
return SellCheckTuple(sell_flag=True, sell_type=selltype)
|
||||||
|
|
||||||
|
# update the stop loss afterwards, after all by definition it's supposed to be hanging
|
||||||
|
if trailing_stop:
|
||||||
|
|
||||||
|
# check if we have a special stop loss for positive condition
|
||||||
|
# and if profit is positive
|
||||||
|
stop_loss_value = self.stoploss
|
||||||
|
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
|
||||||
|
|
||||||
|
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
||||||
|
|
||||||
|
# Ignore mypy error check in configuration that this is a float
|
||||||
|
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
|
||||||
|
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
|
||||||
|
f"with offset {sl_offset:.4g} "
|
||||||
|
f"since we have profit {current_profit:.4f}%")
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||||
|
|
||||||
|
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||||
|
|
||||||
|
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
|
||||||
|
"""
|
||||||
|
Based an earlier trade and current price and ROI configuration, decides whether bot should
|
||||||
|
sell
|
||||||
|
:return True if bot should sell at current rate
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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.minimal_roi.items():
|
||||||
|
if time_diff <= duration:
|
||||||
|
return False
|
||||||
|
if current_profit > threshold:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||||
|
"""
|
||||||
|
Creates a dataframe and populates indicators for given ticker data
|
||||||
|
"""
|
||||||
|
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
|
||||||
|
for pair, pair_data in tickerdata.items()}
|
||||||
|
|
||||||
|
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Populate indicators that will be used in the Buy and Sell strategy
|
||||||
|
This method should not be overridden.
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
|
"""
|
||||||
|
if self._populate_fun_len == 2:
|
||||||
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
|
"the current function headers!", DeprecationWarning)
|
||||||
|
return self.populate_indicators(dataframe) # type: ignore
|
||||||
|
else:
|
||||||
|
return self.populate_indicators(dataframe, metadata)
|
||||||
|
|
||||||
|
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
|
This method should not be overridden.
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:param pair: Additional information, like the currently traded pair
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
if self._buy_fun_len == 2:
|
||||||
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
|
"the current function headers!", DeprecationWarning)
|
||||||
|
return self.populate_buy_trend(dataframe) # type: ignore
|
||||||
|
else:
|
||||||
|
return self.populate_buy_trend(dataframe, metadata)
|
||||||
|
|
||||||
|
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
This method should not be overridden.
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:param pair: Additional information, like the currently traded pair
|
||||||
|
:return: DataFrame with sell column
|
||||||
|
"""
|
||||||
|
if self._sell_fun_len == 2:
|
||||||
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
|
"the current function headers!", DeprecationWarning)
|
||||||
|
return self.populate_sell_trend(dataframe) # type: ignore
|
||||||
|
else:
|
||||||
|
return self.populate_sell_trend(dataframe, metadata)
|
||||||
|
@ -7,14 +7,16 @@ import importlib.util
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
from base64 import urlsafe_b64decode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import Optional, Dict, Type
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional, Type
|
||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.strategy import import_strategy
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ class StrategyResolver(object):
|
|||||||
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
|
||||||
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
|
||||||
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
self.strategy: IStrategy = self._load_strategy(strategy_name,
|
||||||
|
config=config,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config.get('strategy_path'))
|
||||||
|
|
||||||
# Set attributes
|
# Set attributes
|
||||||
@ -42,12 +45,16 @@ class StrategyResolver(object):
|
|||||||
if 'minimal_roi' in config:
|
if 'minimal_roi' in config:
|
||||||
self.strategy.minimal_roi = config['minimal_roi']
|
self.strategy.minimal_roi = config['minimal_roi']
|
||||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
||||||
|
else:
|
||||||
|
config['minimal_roi'] = self.strategy.minimal_roi
|
||||||
|
|
||||||
if 'stoploss' in config:
|
if 'stoploss' in config:
|
||||||
self.strategy.stoploss = config['stoploss']
|
self.strategy.stoploss = config['stoploss']
|
||||||
logger.info(
|
logger.info(
|
||||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
config['stoploss'] = self.strategy.stoploss
|
||||||
|
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
self.strategy.ticker_interval = config['ticker_interval']
|
self.strategy.ticker_interval = config['ticker_interval']
|
||||||
@ -55,6 +62,8 @@ class StrategyResolver(object):
|
|||||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
||||||
config['ticker_interval']
|
config['ticker_interval']
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
config['ticker_interval'] = self.strategy.ticker_interval
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
@ -63,10 +72,11 @@ class StrategyResolver(object):
|
|||||||
self.strategy.stoploss = float(self.strategy.stoploss)
|
self.strategy.stoploss = float(self.strategy.stoploss)
|
||||||
|
|
||||||
def _load_strategy(
|
def _load_strategy(
|
||||||
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
|
self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Search and loads the specified strategy.
|
Search and loads the specified strategy.
|
||||||
:param strategy_name: name of the module to import
|
:param strategy_name: name of the module to import
|
||||||
|
:param config: configuration for the strategy
|
||||||
:param extra_dir: additional directory to search for the given strategy
|
:param extra_dir: additional directory to search for the given strategy
|
||||||
:return: Strategy instance or None
|
:return: Strategy instance or None
|
||||||
"""
|
"""
|
||||||
@ -80,12 +90,35 @@ class StrategyResolver(object):
|
|||||||
# Add extra strategy directory on top of search paths
|
# Add extra strategy directory on top of search paths
|
||||||
abs_paths.insert(0, extra_dir)
|
abs_paths.insert(0, extra_dir)
|
||||||
|
|
||||||
|
if ":" in strategy_name:
|
||||||
|
logger.info("loading base64 endocded strategy")
|
||||||
|
strat = strategy_name.split(":")
|
||||||
|
|
||||||
|
if len(strat) == 2:
|
||||||
|
temp = Path(tempfile.mkdtemp("freq", "strategy"))
|
||||||
|
name = strat[0] + ".py"
|
||||||
|
|
||||||
|
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
|
||||||
|
temp.joinpath("__init__.py").touch()
|
||||||
|
|
||||||
|
strategy_name = os.path.splitext(name)[0]
|
||||||
|
|
||||||
|
# register temp path with the bot
|
||||||
|
abs_paths.insert(0, str(temp.resolve()))
|
||||||
|
|
||||||
for path in abs_paths:
|
for path in abs_paths:
|
||||||
try:
|
try:
|
||||||
strategy = self._search_strategy(path, strategy_name)
|
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
|
||||||
if strategy:
|
if strategy:
|
||||||
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
|
||||||
return import_strategy(strategy)
|
strategy._populate_fun_len = len(
|
||||||
|
inspect.getfullargspec(strategy.populate_indicators).args)
|
||||||
|
strategy._buy_fun_len = len(
|
||||||
|
inspect.getfullargspec(strategy.populate_buy_trend).args)
|
||||||
|
strategy._sell_fun_len = len(
|
||||||
|
inspect.getfullargspec(strategy.populate_sell_trend).args)
|
||||||
|
|
||||||
|
return import_strategy(strategy, config=config)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.warning('Path "%s" does not exist', path)
|
logger.warning('Path "%s" does not exist', path)
|
||||||
|
|
||||||
@ -115,7 +148,7 @@ class StrategyResolver(object):
|
|||||||
return next(valid_strategies_gen, None)
|
return next(valid_strategies_gen, None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]:
|
def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]:
|
||||||
"""
|
"""
|
||||||
Search for the strategy_name in the given directory
|
Search for the strategy_name in the given directory
|
||||||
:param directory: relative or absolute directory path
|
:param directory: relative or absolute directory path
|
||||||
@ -131,5 +164,5 @@ class StrategyResolver(object):
|
|||||||
os.path.abspath(os.path.join(directory, entry)), strategy_name
|
os.path.abspath(os.path.join(directory, entry)), strategy_name
|
||||||
)
|
)
|
||||||
if strategy:
|
if strategy:
|
||||||
return strategy()
|
return strategy(config)
|
||||||
return None
|
return None
|
||||||
|
@ -2,17 +2,15 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from typing import Dict, Optional
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
|
||||||
from telegram import Chat, Message, Update
|
from telegram import Chat, Message, Update
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
from freqtrade import constants
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
|
|
||||||
@ -20,7 +18,7 @@ logging.getLogger('').setLevel(logging.INFO)
|
|||||||
|
|
||||||
|
|
||||||
def log_has(line, logs):
|
def log_has(line, logs):
|
||||||
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
|
# caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar')
|
||||||
# and we want to match line against foobar in the tuple
|
# and we want to match line against foobar in the tuple
|
||||||
return reduce(lambda a, b: a or b,
|
return reduce(lambda a, b: a or b,
|
||||||
filter(lambda x: x[2] == line, logs),
|
filter(lambda x: x[2] == line, logs),
|
||||||
@ -29,6 +27,7 @@ def log_has(line, logs):
|
|||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None) -> None:
|
def patch_exchange(mocker, api_mock=None) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
@ -51,13 +50,11 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
|
|||||||
"""
|
"""
|
||||||
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
|
||||||
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
|
||||||
patch_exchange(mocker, None)
|
patch_exchange(mocker, None)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
|
|
||||||
|
|
||||||
return FreqtradeBot(config)
|
return FreqtradeBot(config)
|
||||||
|
|
||||||
@ -100,7 +97,10 @@ def default_conf():
|
|||||||
"0": 0.04
|
"0": 0.04
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": 600,
|
"unfilledtimeout": {
|
||||||
|
"buy": 10,
|
||||||
|
"sell": 30
|
||||||
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
@ -125,7 +125,6 @@ def default_conf():
|
|||||||
"db_url": "sqlite://",
|
"db_url": "sqlite://",
|
||||||
"loglevel": logging.DEBUG,
|
"loglevel": logging.DEBUG,
|
||||||
}
|
}
|
||||||
validate(configuration, constants.CONF_SCHEMA)
|
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
@ -613,7 +612,7 @@ def tickers():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
|
||||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
# FIX:
|
# FIX:
|
||||||
# Create an fixture/function
|
# Create an fixture/function
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
|
||||||
# pragma pylint: disable=protected-access
|
# pragma pylint: disable=protected-access
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
|
||||||
from random import randint
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from random import randint
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade import OperationalException, DependencyException, TemporaryError
|
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange, API_RETRY_COUNT
|
from freqtrade.exchange import API_RETRY_COUNT, Exchange
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
|
|
||||||
|
|
||||||
|
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
getattr(exchange, fun)(**kwargs)
|
||||||
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
getattr(exchange, fun)(**kwargs)
|
||||||
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
@ -20,7 +33,7 @@ def test_init(default_conf, mocker, caplog):
|
|||||||
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_init_exception(default_conf):
|
def test_init_exception(default_conf, mocker):
|
||||||
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
@ -28,6 +41,100 @@ def test_init_exception(default_conf):
|
|||||||
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||||
|
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_symbol_amount_prec(default_conf, mocker):
|
||||||
|
'''
|
||||||
|
Test rounds down to 4 Decimal places
|
||||||
|
'''
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
|
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
|
||||||
|
|
||||||
|
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
|
||||||
|
type(api_mock).markets = markets
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
amount = 2.34559
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
amount = exchange.symbol_amount_prec(pair, amount)
|
||||||
|
assert amount == 2.3455
|
||||||
|
|
||||||
|
|
||||||
|
def test_symbol_price_prec(default_conf, mocker):
|
||||||
|
'''
|
||||||
|
Test rounds up to 4 decimal places
|
||||||
|
'''
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
|
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
|
||||||
|
|
||||||
|
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
|
||||||
|
type(api_mock).markets = markets
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
price = 2.34559
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
price = exchange.symbol_price_prec(pair, price)
|
||||||
|
assert price == 2.3456
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_sandbox(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test working scenario
|
||||||
|
"""
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
|
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||||
|
})
|
||||||
|
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
|
||||||
|
'api': 'https://api.gdax.com'})
|
||||||
|
type(api_mock).urls = url_mock
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
liveurl = exchange._api.urls['api']
|
||||||
|
default_conf['exchange']['sandbox'] = True
|
||||||
|
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||||
|
assert exchange._api.urls['api'] != liveurl
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_sandbox_exception(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test Fail scenario
|
||||||
|
"""
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
|
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
|
||||||
|
})
|
||||||
|
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
|
||||||
|
type(api_mock).urls = url_mock
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
default_conf['exchange']['sandbox'] = True
|
||||||
|
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker):
|
def test_validate_pairs(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -38,6 +145,7 @@ def test_validate_pairs(default_conf, mocker):
|
|||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -45,6 +153,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
|
|||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -55,12 +164,11 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
api_mock.load_markets = MagicMock(return_value={
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
|
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
|
||||||
})
|
})
|
||||||
conf = deepcopy(default_conf)
|
default_conf['stake_currency'] = 'ETH'
|
||||||
conf['stake_currency'] = 'ETH'
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
Exchange(conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||||
@ -70,6 +178,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -84,17 +193,80 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
conf = deepcopy(default_conf)
|
default_conf['stake_currency'] = 'ETH'
|
||||||
conf['stake_currency'] = 'ETH'
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.name = MagicMock(return_value='binance')
|
api_mock.name = MagicMock(return_value='binance')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
|
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
|
||||||
):
|
):
|
||||||
Exchange(conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes(default_conf, mocker):
|
||||||
|
default_conf["ticker_interval"] = "5m"
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes_failed(default_conf, mocker):
|
||||||
|
default_conf["ticker_interval"] = "3m"
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
|
del default_conf["ticker_interval"]
|
||||||
|
api_mock = MagicMock()
|
||||||
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
|
type(api_mock).id = id_mock
|
||||||
|
timeframes = PropertyMock(return_value={'1m': '1m',
|
||||||
|
'5m': '5m',
|
||||||
|
'15m': '15m',
|
||||||
|
'1h': '1h'})
|
||||||
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_exchange_has(default_conf, mocker):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
assert not exchange.exchange_has('ASDFASDF')
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'deadbeef': True})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
assert exchange.exchange_has("deadbeef")
|
||||||
|
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'deadbeef': False})
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
assert not exchange.exchange_has("deadbeef")
|
||||||
|
|
||||||
|
|
||||||
def test_buy_dry_run(default_conf, mocker):
|
def test_buy_dry_run(default_conf, mocker):
|
||||||
@ -216,6 +388,11 @@ def test_get_balance_prod(default_conf, mocker):
|
|||||||
|
|
||||||
exchange.get_balance(currency='BTC')
|
exchange.get_balance(currency='BTC')
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
|
||||||
|
exchange.get_balance(currency='BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_balances_dry_run(default_conf, mocker):
|
def test_get_balances_dry_run(default_conf, mocker):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
@ -243,17 +420,8 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
assert exchange.get_balances()['1ST']['total'] == 10.0
|
assert exchange.get_balances()['1ST']['total'] == 10.0
|
||||||
assert exchange.get_balances()['1ST']['used'] == 0.0
|
assert exchange.get_balances()['1ST']['used'] == 0.0
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
|
"get_balances", "fetch_balance")
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_balances()
|
|
||||||
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
|
||||||
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_balances()
|
|
||||||
assert api_mock.fetch_balance.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_tickers(default_conf, mocker):
|
def test_get_tickers(default_conf, mocker):
|
||||||
@ -282,15 +450,8 @@ def test_get_tickers(default_conf, mocker):
|
|||||||
assert tickers['BCH/BTC']['bid'] == 0.6
|
assert tickers['BCH/BTC']['bid'] == 0.6
|
||||||
assert tickers['BCH/BTC']['ask'] == 0.5
|
assert tickers['BCH/BTC']['ask'] == 0.5
|
||||||
|
|
||||||
with pytest.raises(TemporaryError): # test retrier
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError)
|
"get_tickers", "fetch_tickers")
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_tickers()
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
|
||||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_tickers()
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
|
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
|
||||||
@ -345,15 +506,9 @@ def test_get_ticker(default_conf, mocker):
|
|||||||
exchange.get_ticker(pair='ETH/BTC', refresh=False)
|
exchange.get_ticker(pair='ETH/BTC', refresh=False)
|
||||||
assert api_mock.fetch_ticker.call_count == 0
|
assert api_mock.fetch_ticker.call_count == 0
|
||||||
|
|
||||||
with pytest.raises(TemporaryError): # test retrier
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
|
"get_ticker", "fetch_ticker",
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
pair='ETH/BTC', refresh=True)
|
||||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
|
||||||
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
|
||||||
|
|
||||||
api_mock.fetch_ticker = MagicMock(return_value={})
|
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
@ -369,7 +524,7 @@ def make_fetch_ohlcv_mock(data):
|
|||||||
return fetch_ohlcv_mock
|
return fetch_ohlcv_mock
|
||||||
|
|
||||||
|
|
||||||
def test_get_ticker_history(default_conf, mocker):
|
def test_get_candle_history(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
tick = [
|
tick = [
|
||||||
[
|
[
|
||||||
@ -386,7 +541,7 @@ def test_get_ticker_history(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
# retrieve original ticker
|
# retrieve original ticker
|
||||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert ticks[0][0] == 1511686200000
|
assert ticks[0][0] == 1511686200000
|
||||||
assert ticks[0][1] == 1
|
assert ticks[0][1] == 1
|
||||||
assert ticks[0][2] == 2
|
assert ticks[0][2] == 2
|
||||||
@ -408,7 +563,7 @@ def test_get_ticker_history(default_conf, mocker):
|
|||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert ticks[0][0] == 1511686210000
|
assert ticks[0][0] == 1511686210000
|
||||||
assert ticks[0][1] == 6
|
assert ticks[0][1] == 6
|
||||||
assert ticks[0][2] == 7
|
assert ticks[0][2] == 7
|
||||||
@ -416,20 +571,17 @@ def test_get_ticker_history(default_conf, mocker):
|
|||||||
assert ticks[0][4] == 9
|
assert ticks[0][4] == 9
|
||||||
assert ticks[0][5] == 10
|
assert ticks[0][5] == 10
|
||||||
|
|
||||||
with pytest.raises(TemporaryError): # test retrier
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
|
"get_candle_history", "fetch_ohlcv",
|
||||||
|
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
# new symbol to get around cache
|
exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||||
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
|
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
|
||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
# new symbol to get around cache
|
|
||||||
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_ticker_history_sort(default_conf, mocker):
|
def test_get_candle_history_sort(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
|
|
||||||
# GDAX use-case (real data from GDAX)
|
# GDAX use-case (real data from GDAX)
|
||||||
@ -452,7 +604,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert ticks[0][0] == 1527830400000
|
assert ticks[0][0] == 1527830400000
|
||||||
assert ticks[0][1] == 0.07649
|
assert ticks[0][1] == 0.07649
|
||||||
assert ticks[0][2] == 0.07651
|
assert ticks[0][2] == 0.07651
|
||||||
@ -485,7 +637,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
|
|||||||
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
# Test the ticker history sort
|
# Test the ticker history sort
|
||||||
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
|
ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
|
||||||
assert ticks[0][0] == 1527827700000
|
assert ticks[0][0] == 1527827700000
|
||||||
assert ticks[0][1] == 0.07659999
|
assert ticks[0][1] == 0.07659999
|
||||||
assert ticks[0][2] == 0.0766
|
assert ticks[0][2] == 0.0766
|
||||||
@ -515,24 +667,15 @@ def test_cancel_order(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
|
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
|
||||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
|
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
|
||||||
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||||
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
|
"cancel_order", "cancel_order",
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
order_id='_', pair='TKN/BTC')
|
||||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
|
||||||
assert api_mock.cancel_order.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_order(default_conf, mocker):
|
def test_get_order(default_conf, mocker):
|
||||||
@ -550,23 +693,15 @@ def test_get_order(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
assert exchange.get_order('X', 'TKN/BTC') == 456
|
assert exchange.get_order('X', 'TKN/BTC') == 456
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
|
||||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
|
'get_order', 'fetch_order',
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
order_id='_', pair='TKN/BTC')
|
||||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
|
||||||
assert api_mock.fetch_order.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_name(default_conf, mocker):
|
def test_name(default_conf, mocker):
|
||||||
@ -651,19 +786,12 @@ def test_get_trades_for_order(default_conf, mocker):
|
|||||||
assert len(orders) == 1
|
assert len(orders) == 1
|
||||||
assert orders[0]['price'] == 165
|
assert orders[0]['price'] == 165
|
||||||
|
|
||||||
# test Exceptions
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
with pytest.raises(OperationalException):
|
'get_trades_for_order', 'fetch_my_trades',
|
||||||
api_mock = MagicMock()
|
order_id=order_id, pair='LTC/BTC', since=since)
|
||||||
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
|
||||||
api_mock = MagicMock()
|
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
|
||||||
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
|
||||||
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_markets(default_conf, mocker, markets):
|
def test_get_markets(default_conf, mocker, markets):
|
||||||
@ -677,19 +805,8 @@ def test_get_markets(default_conf, mocker, markets):
|
|||||||
assert ret[0]["id"] == "ethbtc"
|
assert ret[0]["id"] == "ethbtc"
|
||||||
assert ret[0]["symbol"] == "ETH/BTC"
|
assert ret[0]["symbol"] == "ETH/BTC"
|
||||||
|
|
||||||
# test Exceptions
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
with pytest.raises(OperationalException):
|
'get_markets', 'fetch_markets')
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_markets()
|
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_markets()
|
|
||||||
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_fee(default_conf, mocker):
|
def test_get_fee(default_conf, mocker):
|
||||||
@ -704,19 +821,8 @@ def test_get_fee(default_conf, mocker):
|
|||||||
|
|
||||||
assert exchange.get_fee() == 0.025
|
assert exchange.get_fee() == 0.025
|
||||||
|
|
||||||
# test Exceptions
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
with pytest.raises(OperationalException):
|
'get_fee', 'calculate_fee')
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_fee()
|
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
exchange.get_fee()
|
|
||||||
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_amount_lots(default_conf, mocker):
|
def test_get_amount_lots(default_conf, mocker):
|
||||||
|
21
freqtrade/tests/exchange/test_exchange_helpers.py
Normal file
21
freqtrade/tests/exchange/test_exchange_helpers.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_correct_length(result):
|
||||||
|
dataframe = parse_ticker_dataframe(result)
|
||||||
|
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_correct_columns(result):
|
||||||
|
assert result.columns.tolist() == \
|
||||||
|
['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_ticker_dataframe(ticker_history):
|
||||||
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
|
|
||||||
|
# Test file with BV data
|
||||||
|
dataframe = parse_ticker_dataframe(ticker_history)
|
||||||
|
assert dataframe.columns.tolist() == columns
|
@ -3,20 +3,21 @@
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
import pytest
|
|
||||||
from copy import deepcopy
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
from arrow import Arrow
|
from arrow import Arrow
|
||||||
|
|
||||||
from freqtrade import optimize, constants, DependencyException
|
from freqtrade import DependencyException, constants, optimize
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
|
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
|
||||||
|
start)
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
from freqtrade.strategy.interface import SellType
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
def get_args(args) -> List[str]:
|
def get_args(args) -> List[str]:
|
||||||
@ -95,7 +96,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
|
|||||||
'stake_amount': config['stake_amount'],
|
'stake_amount': config['stake_amount'],
|
||||||
'processed': processed,
|
'processed': processed,
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'realistic': True
|
'position_stacking': False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
@ -109,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
|||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
# use for mock freqtrade.exchange.get_ticker_history'
|
# use for mock freqtrade.exchange.get_candle_history'
|
||||||
def _load_pair_as_ticks(pair, tickfreq):
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
ticks = trim_dictlist(ticks, -201)
|
ticks = trim_dictlist(ticks, -201)
|
||||||
@ -126,7 +127,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
|
|||||||
'stake_amount': conf['stake_amount'],
|
'stake_amount': conf['stake_amount'],
|
||||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'realistic': True,
|
'position_stacking': False,
|
||||||
'record': record
|
'record': record
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +145,7 @@ def _trend(signals, buy_value, sell_value):
|
|||||||
return signals
|
return signals
|
||||||
|
|
||||||
|
|
||||||
def _trend_alternate(dataframe=None):
|
def _trend_alternate(dataframe=None, metadata=None):
|
||||||
signals = dataframe
|
signals = dataframe
|
||||||
low = signals['low']
|
low = signals['low']
|
||||||
n = len(low)
|
n = len(low)
|
||||||
@ -162,9 +163,6 @@ def _trend_alternate(dataframe=None):
|
|||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test setup_configuration() function
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -192,8 +190,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'live' not in config
|
assert 'live' not in config
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'realistic_simulation' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
@ -203,9 +201,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test setup_configuration() function
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -217,7 +212,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--realistic-simulation',
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--export', '/bar/foo',
|
'--export', '/bar/foo',
|
||||||
@ -245,9 +241,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'live' in config
|
assert 'live' in config
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'realistic_simulation' in config
|
assert 'position_stacking' in config
|
||||||
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
|
||||||
|
assert 'use_max_market_positions' in config
|
||||||
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'refresh_pairs' in config
|
assert 'refresh_pairs' in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
@ -270,15 +269,10 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
Test setup_configuration() function
|
|
||||||
"""
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
@ -292,9 +286,6 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
|
|||||||
|
|
||||||
|
|
||||||
def test_start(mocker, fee, default_conf, caplog) -> None:
|
def test_start(mocker, fee, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test start() function
|
|
||||||
"""
|
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -317,26 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_backtesting_init(mocker, default_conf) -> None:
|
def test_backtesting_init(mocker, default_conf) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting._init() method
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
assert backtesting.config == default_conf
|
assert backtesting.config == default_conf
|
||||||
assert isinstance(backtesting.analyze, Analyze)
|
|
||||||
assert backtesting.ticker_interval == '5m'
|
assert backtesting.ticker_interval == '5m'
|
||||||
assert callable(backtesting.tickerdata_to_dataframe)
|
assert callable(backtesting.tickerdata_to_dataframe)
|
||||||
assert callable(backtesting.populate_buy_trend)
|
assert callable(backtesting.advise_buy)
|
||||||
assert callable(backtesting.populate_sell_trend)
|
assert callable(backtesting.advise_sell)
|
||||||
get_fee.assert_called()
|
get_fee.assert_called()
|
||||||
assert backtesting.fee == 0.5
|
assert backtesting.fee == 0.5
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.tickerdata_to_dataframe() method
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
@ -346,16 +330,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
|
|||||||
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
data = backtesting.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99
|
assert len(data['UNITTEST/BTC']) == 99
|
||||||
|
|
||||||
# Load Analyze to compare the result between Backtesting function and Analyze are the same
|
# Load strategy to compare the result between Backtesting function and strategy are the same
|
||||||
analyze = Analyze(default_conf)
|
strategy = DefaultStrategy(default_conf)
|
||||||
data2 = analyze.tickerdata_to_dataframe(tickerlist)
|
data2 = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
|
||||||
|
|
||||||
|
|
||||||
def test_get_timeframe(default_conf, mocker) -> None:
|
def test_get_timeframe(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.get_timeframe() method
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
@ -372,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_text_table(default_conf, mocker):
|
def test_generate_text_table(default_conf, mocker):
|
||||||
"""
|
|
||||||
Test Backtesting.generate_text_table() method
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
@ -390,29 +368,94 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| pair | buy count | avg profit % | '
|
'| pair | buy count | avg profit % | cum profit % | '
|
||||||
'total profit BTC | avg duration | profit | loss |\n'
|
'total profit BTC | avg duration | profit | loss |\n'
|
||||||
'|:--------|------------:|---------------:|'
|
'|:--------|------------:|---------------:|---------------:|'
|
||||||
'-------------------:|---------------:|---------:|-------:|\n'
|
'-------------------:|:---------------|---------:|-------:|\n'
|
||||||
'| ETH/BTC | 2 | 15.00 | '
|
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
||||||
'0.60000000 | 20.0 | 2 | 0 |\n'
|
'0.60000000 | 0:20:00 | 2 | 0 |\n'
|
||||||
'| TOTAL | 2 | 15.00 | '
|
'| TOTAL | 2 | 15.00 | 30.00 | '
|
||||||
'0.60000000 | 20.0 | 2 | 0 |'
|
'0.60000000 | 0:20:00 | 2 | 0 |'
|
||||||
)
|
)
|
||||||
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
def test_generate_text_table_sell_reason(default_conf, mocker):
|
||||||
"""
|
patch_exchange(mocker)
|
||||||
Test Backtesting.start() method
|
backtesting = Backtesting(default_conf)
|
||||||
"""
|
|
||||||
|
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [0.1, 0.2, 0.3],
|
||||||
|
'profit_abs': [0.2, 0.4, 0.5],
|
||||||
|
'trade_duration': [10, 30, 10],
|
||||||
|
'profit': [2, 0, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'| Sell Reason | Count |\n'
|
||||||
|
'|:--------------|--------:|\n'
|
||||||
|
'| roi | 2 |\n'
|
||||||
|
'| stop_loss | 1 |'
|
||||||
|
)
|
||||||
|
assert backtesting._generate_text_table_sell_reason(
|
||||||
|
data={'ETH/BTC': {}}, results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table_strategyn(default_conf, mocker):
|
||||||
|
"""
|
||||||
|
Test Backtesting.generate_text_table_sell_reason() method
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
results = {}
|
||||||
|
results['ETH/BTC'] = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
|
||||||
|
'profit_percent': [0.1, 0.2, 0.3],
|
||||||
|
'profit_abs': [0.2, 0.4, 0.5],
|
||||||
|
'trade_duration': [10, 30, 10],
|
||||||
|
'profit': [2, 0, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
results['LTC/BTC'] = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
|
||||||
|
'profit_percent': [0.4, 0.2, 0.3],
|
||||||
|
'profit_abs': [0.4, 0.4, 0.5],
|
||||||
|
'trade_duration': [15, 30, 15],
|
||||||
|
'profit': [4, 1, 0],
|
||||||
|
'loss': [0, 0, 1],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result_str = (
|
||||||
|
'| Strategy | buy count | avg profit % | cum profit % '
|
||||||
|
'| total profit BTC | avg duration | profit | loss |\n'
|
||||||
|
'|:-----------|------------:|---------------:|---------------:'
|
||||||
|
'|-------------------:|:---------------|---------:|-------:|\n'
|
||||||
|
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||||
|
'| 1.10000000 | 0:17:00 | 3 | 0 |\n'
|
||||||
|
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||||
|
'| 1.30000000 | 0:20:00 | 3 | 0 |'
|
||||||
|
)
|
||||||
|
print(backtesting._generate_text_table_strategy(all_results=results))
|
||||||
|
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
def get_timeframe(input1, input2):
|
def get_timeframe(input1, input2):
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
|
||||||
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
@ -421,15 +464,14 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
get_timeframe=get_timeframe,
|
get_timeframe=get_timeframe,
|
||||||
)
|
)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['ticker_interval'] = 1
|
||||||
conf['ticker_interval'] = 1
|
default_conf['live'] = False
|
||||||
conf['live'] = False
|
default_conf['datadir'] = None
|
||||||
conf['datadir'] = None
|
default_conf['export'] = None
|
||||||
conf['export'] = None
|
default_conf['timerange'] = '-100'
|
||||||
conf['timerange'] = '-100'
|
|
||||||
|
|
||||||
backtesting = Backtesting(conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
exists = [
|
exists = [
|
||||||
@ -444,16 +486,11 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.start() method if no data is found
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_timeframe(input1, input2):
|
def get_timeframe(input1, input2):
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
||||||
|
|
||||||
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
|
|
||||||
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
|
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
@ -462,15 +499,14 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
|||||||
get_timeframe=get_timeframe,
|
get_timeframe=get_timeframe,
|
||||||
)
|
)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
default_conf['ticker_interval'] = "1m"
|
||||||
conf['ticker_interval'] = "1m"
|
default_conf['live'] = False
|
||||||
conf['live'] = False
|
default_conf['datadir'] = None
|
||||||
conf['datadir'] = None
|
default_conf['export'] = None
|
||||||
conf['export'] = None
|
default_conf['timerange'] = '20180101-20180102'
|
||||||
conf['timerange'] = '20180101-20180102'
|
|
||||||
|
|
||||||
backtesting = Backtesting(conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.start()
|
backtesting.start()
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
|
|
||||||
@ -478,31 +514,53 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, fee, mocker) -> None:
|
def test_backtest(default_conf, fee, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.backtest() method
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
pair = 'UNITTEST/BTC'
|
||||||
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
|
||||||
data = trim_dictlist(data, -200)
|
data = trim_dictlist(data, -200)
|
||||||
|
data_processed = backtesting.tickerdata_to_dataframe(data)
|
||||||
results = backtesting.backtest(
|
results = backtesting.backtest(
|
||||||
{
|
{
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
'processed': data_processed,
|
||||||
'max_open_trades': 10,
|
'max_open_trades': 10,
|
||||||
'realistic': True
|
'position_stacking': False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
|
|
||||||
|
expected = pd.DataFrame(
|
||||||
|
{'pair': [pair, pair],
|
||||||
|
'profit_percent': [0.00029975, 0.00056708],
|
||||||
|
'profit_abs': [1.49e-06, 7.6e-07],
|
||||||
|
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
|
||||||
|
Arrow(2018, 1, 30, 3, 30, 0).datetime],
|
||||||
|
'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime,
|
||||||
|
Arrow(2018, 1, 30, 4, 20, 0).datetime],
|
||||||
|
'open_index': [77, 183],
|
||||||
|
'close_index': [125, 193],
|
||||||
|
'trade_duration': [240, 50],
|
||||||
|
'open_at_end': [False, False],
|
||||||
|
'open_rate': [0.104445, 0.10302485],
|
||||||
|
'close_rate': [0.105, 0.10359999],
|
||||||
|
'sell_reason': [SellType.ROI, SellType.ROI]
|
||||||
|
})
|
||||||
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
|
data_pair = data_processed[pair]
|
||||||
|
for _, t in results.iterrows():
|
||||||
|
ln = data_pair.loc[data_pair["date"] == t["open_time"]]
|
||||||
|
# Check open trade rate alignes to open rate
|
||||||
|
assert ln is not None
|
||||||
|
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
|
||||||
|
# check close trade rate alignes to close rate
|
||||||
|
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
|
||||||
|
assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6)
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.backtest() method with 1 min ticker
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
@ -515,7 +573,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
|||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
'processed': backtesting.tickerdata_to_dataframe(data),
|
'processed': backtesting.tickerdata_to_dataframe(data),
|
||||||
'max_open_trades': 1,
|
'max_open_trades': 1,
|
||||||
'realistic': True
|
'position_stacking': False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
@ -523,9 +581,6 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_processed(default_conf, mocker) -> None:
|
def test_processed(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Backtesting.backtest() method with offline data
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
|
|
||||||
@ -551,42 +606,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
ticks = [1, 5]
|
ticks = [1, 5]
|
||||||
fun = Backtesting(default_conf).populate_buy_trend
|
fun = Backtesting(default_conf).advise_buy
|
||||||
for _ in ticks:
|
for _ in ticks:
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = fun # Override
|
backtesting.advise_buy = fun # Override
|
||||||
backtesting.populate_sell_trend = fun # Override
|
backtesting.advise_sell = fun # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert not results.empty
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_clash_buy_sell(mocker, default_conf):
|
def test_backtest_clash_buy_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None, pair=None):
|
||||||
buy_value = 1
|
buy_value = 1
|
||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = fun # Override
|
backtesting.advise_buy = fun # Override
|
||||||
backtesting.populate_sell_trend = fun # Override
|
backtesting.advise_sell = fun # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_only_sell(mocker, default_conf):
|
def test_backtest_only_sell(mocker, default_conf):
|
||||||
# Override the default buy trend function in our default_strategy
|
# Override the default buy trend function in our default_strategy
|
||||||
def fun(dataframe=None):
|
def fun(dataframe=None, pair=None):
|
||||||
buy_value = 0
|
buy_value = 0
|
||||||
sell_value = 1
|
sell_value = 1
|
||||||
return _trend(dataframe, buy_value, sell_value)
|
return _trend(dataframe, buy_value, sell_value)
|
||||||
|
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = fun # Override
|
backtesting.advise_buy = fun # Override
|
||||||
backtesting.populate_sell_trend = fun # Override
|
backtesting.advise_sell = fun # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert results.empty
|
assert results.empty
|
||||||
|
|
||||||
@ -595,8 +650,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
|
||||||
backtesting = Backtesting(default_conf)
|
backtesting = Backtesting(default_conf)
|
||||||
backtesting.populate_buy_trend = _trend_alternate # Override
|
backtesting.advise_buy = _trend_alternate # Override
|
||||||
backtesting.populate_sell_trend = _trend_alternate # Override
|
backtesting.advise_sell = _trend_alternate # Override
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
backtesting._store_backtest_result("test_.json", results)
|
backtesting._store_backtest_result("test_.json", results)
|
||||||
assert len(results) == 4
|
assert len(results) == 4
|
||||||
@ -627,9 +682,15 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
Arrow(2017, 11, 14, 22, 10, 00).datetime,
|
||||||
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
Arrow(2017, 11, 14, 22, 43, 00).datetime,
|
||||||
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
Arrow(2017, 11, 14, 22, 58, 00).datetime],
|
||||||
|
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
|
||||||
|
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
|
||||||
"open_index": [1, 119, 153, 185],
|
"open_index": [1, 119, 153, 185],
|
||||||
"close_index": [118, 151, 184, 199],
|
"close_index": [118, 151, 184, 199],
|
||||||
"trade_duration": [123, 34, 31, 14]})
|
"trade_duration": [123, 34, 31, 14],
|
||||||
|
"open_at_end": [False, False, False, True],
|
||||||
|
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
|
||||||
|
SellType.ROI, SellType.FORCE_SELL]
|
||||||
|
})
|
||||||
backtesting._store_backtest_result("backtest-result.json", results)
|
backtesting._store_backtest_result("backtest-result.json", results)
|
||||||
assert len(results) == 4
|
assert len(results) == 4
|
||||||
# Assert file_dump_json was only called once
|
# Assert file_dump_json was only called once
|
||||||
@ -637,15 +698,32 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
records = records[0]
|
records = records[0]
|
||||||
# Ensure records are of correct type
|
# Ensure records are of correct type
|
||||||
assert len(records) == 4
|
assert len(records) == 4
|
||||||
|
|
||||||
|
# reset test to test with strategy name
|
||||||
|
names = []
|
||||||
|
records = []
|
||||||
|
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat")
|
||||||
|
assert len(results) == 4
|
||||||
|
# Assert file_dump_json was only called once
|
||||||
|
assert names == ['backtest-result-DefStrat.json']
|
||||||
|
records = records[0]
|
||||||
|
# Ensure records are of correct type
|
||||||
|
assert len(records) == 4
|
||||||
|
|
||||||
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
|
||||||
# Below follows just a typecheck of the schema/type of trade-records
|
# Below follows just a typecheck of the schema/type of trade-records
|
||||||
oix = None
|
oix = None
|
||||||
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
|
for (pair, profit, date_buy, date_sell, buy_index, dur,
|
||||||
|
openr, closer, open_at_end, sell_reason) in records:
|
||||||
assert pair == 'UNITTEST/BTC'
|
assert pair == 'UNITTEST/BTC'
|
||||||
isinstance(profit, float)
|
assert isinstance(profit, float)
|
||||||
# FIX: buy/sell should be converted to ints
|
# FIX: buy/sell should be converted to ints
|
||||||
isinstance(date_buy, str)
|
assert isinstance(date_buy, float)
|
||||||
isinstance(date_sell, str)
|
assert isinstance(date_sell, float)
|
||||||
|
assert isinstance(openr, float)
|
||||||
|
assert isinstance(closer, float)
|
||||||
|
assert isinstance(open_at_end, bool)
|
||||||
|
assert isinstance(sell_reason, str)
|
||||||
isinstance(buy_index, pd._libs.tslib.Timestamp)
|
isinstance(buy_index, pd._libs.tslib.Timestamp)
|
||||||
if oix:
|
if oix:
|
||||||
assert buy_index > oix
|
assert buy_index > oix
|
||||||
@ -654,26 +732,16 @@ def test_backtest_record(default_conf, fee, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
conf = deepcopy(default_conf)
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history',
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
|
|
||||||
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = MagicMock()
|
|
||||||
args.ticker_interval = 1
|
|
||||||
args.level = 10
|
|
||||||
args.live = True
|
|
||||||
args.datadir = None
|
|
||||||
args.export = None
|
|
||||||
args.strategy = 'DefaultStrategy'
|
|
||||||
args.timerange = '-100' # needed due to MagicMock malleability
|
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
@ -682,7 +750,8 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--timerange', '-100',
|
'--timerange', '-100',
|
||||||
'--realistic-simulation'
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions'
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start(args)
|
start(args)
|
||||||
@ -691,14 +760,71 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
|||||||
'Parameter -i/--ticker-interval detected ...',
|
'Parameter -i/--ticker-interval detected ...',
|
||||||
'Using ticker_interval: 1m ...',
|
'Using ticker_interval: 1m ...',
|
||||||
'Parameter -l/--live detected ...',
|
'Parameter -l/--live detected ...',
|
||||||
'Using max_open_trades: 1 ...',
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
'Parameter --timerange detected: -100 ...',
|
'Parameter --timerange detected: -100 ...',
|
||||||
'Using data folder: freqtrade/tests/testdata ...',
|
'Using data folder: freqtrade/tests/testdata ...',
|
||||||
'Using stake_currency: BTC ...',
|
'Using stake_currency: BTC ...',
|
||||||
'Using stake_amount: 0.001 ...',
|
'Using stake_amount: 0.001 ...',
|
||||||
'Downloading data for all pairs in whitelist ...',
|
'Downloading data for all pairs in whitelist ...',
|
||||||
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
'Parameter --realistic-simulation detected ...'
|
'Parameter --enable-position-stacking detected ...'
|
||||||
|
]
|
||||||
|
|
||||||
|
for line in exists:
|
||||||
|
assert log_has(line, caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||||
|
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history',
|
||||||
|
new=lambda s, n, i: _load_pair_as_ticks(n, i))
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtestmock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
gen_table_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
|
||||||
|
gen_strattable_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
|
||||||
|
gen_strattable_mock)
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', 'freqtrade/tests/testdata',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1m',
|
||||||
|
'--live',
|
||||||
|
'--timerange', '-100',
|
||||||
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions',
|
||||||
|
'--strategy-list',
|
||||||
|
'DefaultStrategy',
|
||||||
|
'TestStrategy',
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start(args)
|
||||||
|
# 2 backtests, 4 tables
|
||||||
|
assert backtestmock.call_count == 2
|
||||||
|
assert gen_table_mock.call_count == 4
|
||||||
|
assert gen_strattable_mock.call_count == 1
|
||||||
|
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
exists = [
|
||||||
|
'Parameter -i/--ticker-interval detected ...',
|
||||||
|
'Using ticker_interval: 1m ...',
|
||||||
|
'Parameter -l/--live detected ...',
|
||||||
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
|
'Parameter --timerange detected: -100 ...',
|
||||||
|
'Using data folder: 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:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
|
||||||
|
'Parameter --enable-position-stacking detected ...',
|
||||||
|
'Running backtesting for Strategy DefaultStrategy',
|
||||||
|
'Running backtesting for Strategy TestStrategy',
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in exists:
|
for line in exists:
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212,C0103
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
from copy import deepcopy
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -13,52 +11,32 @@ from freqtrade.strategy.resolver import StrategyResolver
|
|||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
from freqtrade.tests.optimize.test_backtesting import get_args
|
from freqtrade.tests.optimize.test_backtesting import get_args
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
|
||||||
_HYPEROPT_INITIALIZED = False
|
|
||||||
_HYPEROPT = None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
def init_hyperopt(default_conf, mocker):
|
def hyperopt(default_conf, mocker):
|
||||||
global _HYPEROPT_INITIALIZED, _HYPEROPT
|
patch_exchange(mocker)
|
||||||
if not _HYPEROPT_INITIALIZED:
|
return Hyperopt(default_conf)
|
||||||
patch_exchange(mocker)
|
|
||||||
_HYPEROPT = Hyperopt(default_conf)
|
|
||||||
_HYPEROPT_INITIALIZED = True
|
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
def create_trials(mocker) -> None:
|
def create_trials(mocker, hyperopt) -> None:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
"""
|
"""
|
||||||
_HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
|
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||||
|
|
||||||
return mocker.Mock(
|
return [{'loss': 1, 'result': 'foo', 'params': {}}]
|
||||||
results=[
|
|
||||||
{
|
|
||||||
'loss': 1,
|
|
||||||
'result': 'foo',
|
|
||||||
'status': 'ok'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
best_trial={'misc': {'vals': {'adx': 999}}}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Unit tests
|
|
||||||
def test_start(mocker, default_conf, caplog) -> None:
|
def test_start(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test start() function
|
|
||||||
"""
|
|
||||||
start_mock = MagicMock()
|
start_mock = MagicMock()
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
@ -87,11 +65,7 @@ def test_start(mocker, default_conf, caplog) -> None:
|
|||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||||
"""
|
|
||||||
Test Hyperopt.calculate_loss()
|
|
||||||
"""
|
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
|
||||||
@ -101,20 +75,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
|
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
|
||||||
"""
|
|
||||||
Test Hyperopt.calculate_loss()
|
|
||||||
"""
|
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
|
|
||||||
shorter = hyperopt.calculate_loss(1, 100, 20)
|
shorter = hyperopt.calculate_loss(1, 100, 20)
|
||||||
longer = hyperopt.calculate_loss(1, 100, 30)
|
longer = hyperopt.calculate_loss(1, 100, 30)
|
||||||
assert shorter < longer
|
assert shorter < longer
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
def test_loss_calculation_has_limited_profit(hyperopt) -> None:
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
|
|
||||||
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
|
||||||
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, 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)
|
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
|
||||||
@ -122,8 +89,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
hyperopt.log_results(
|
hyperopt.log_results(
|
||||||
{
|
{
|
||||||
@ -134,11 +100,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert ' 1/2: foo. Loss 1.00000'in out
|
assert ' 1/2: foo. Loss 1.00000' in out
|
||||||
|
|
||||||
|
|
||||||
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
hyperopt.current_best_loss = 2
|
hyperopt.current_best_loss = 2
|
||||||
hyperopt.log_results(
|
hyperopt.log_results(
|
||||||
{
|
{
|
||||||
@ -148,166 +113,23 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
|
|||||||
assert caplog.record_tuples == []
|
assert caplog.record_tuples == []
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
|
def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
|
||||||
fmin_result = {
|
trials = create_trials(mocker, hyperopt)
|
||||||
"macd_below_zero": 0,
|
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
|
||||||
"adx": 1,
|
|
||||||
"adx-value": 15.0,
|
|
||||||
"fastd": 1,
|
|
||||||
"fastd-value": 40.0,
|
|
||||||
"green_candle": 1,
|
|
||||||
"mfi": 0,
|
|
||||||
"over_sar": 0,
|
|
||||||
"rsi": 1,
|
|
||||||
"rsi-value": 37.0,
|
|
||||||
"trigger": 2,
|
|
||||||
"uptrend_long_ema": 1,
|
|
||||||
"uptrend_short_ema": 0,
|
|
||||||
"uptrend_sma": 0,
|
|
||||||
"stoploss": -0.1,
|
|
||||||
"roi_t1": 1,
|
|
||||||
"roi_t2": 2,
|
|
||||||
"roi_t3": 3,
|
|
||||||
"roi_p1": 1,
|
|
||||||
"roi_p2": 2,
|
|
||||||
"roi_p3": 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'epochs': 1})
|
|
||||||
conf.update({'timerange': None})
|
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
|
||||||
hyperopt = Hyperopt(conf)
|
|
||||||
hyperopt.trials = create_trials(mocker)
|
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
|
||||||
hyperopt.start()
|
|
||||||
|
|
||||||
exists = [
|
|
||||||
'Best parameters:',
|
|
||||||
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
|
|
||||||
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
|
|
||||||
'"green_candle": {\n "enabled": true\n },',
|
|
||||||
'"macd_below_zero": {\n "enabled": false\n },',
|
|
||||||
'"mfi": {\n "enabled": false\n },',
|
|
||||||
'"over_sar": {\n "enabled": false\n },',
|
|
||||||
'"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:
|
|
||||||
assert line in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'epochs': 1})
|
|
||||||
conf.update({'timerange': None})
|
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
|
||||||
hyperopt = Hyperopt(conf)
|
|
||||||
hyperopt.trials = create_trials(mocker)
|
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
|
||||||
|
|
||||||
hyperopt.start()
|
|
||||||
|
|
||||||
exists = [
|
|
||||||
'Best Result:',
|
|
||||||
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
|
|
||||||
'(param: -e).',
|
|
||||||
]
|
|
||||||
|
|
||||||
for line in exists:
|
|
||||||
assert line in caplog.text
|
|
||||||
|
|
||||||
|
|
||||||
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
|
|
||||||
trials = create_trials(mocker)
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'epochs': 1})
|
|
||||||
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={})
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
|
||||||
hyperopt = Hyperopt(conf)
|
|
||||||
hyperopt.trials = trials
|
hyperopt.trials = trials
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
|
||||||
|
|
||||||
hyperopt.start()
|
|
||||||
|
|
||||||
mock_read.assert_called_once()
|
|
||||||
mock_save.assert_called_once()
|
|
||||||
|
|
||||||
current_tries = hyperopt.current_tries
|
|
||||||
total_tries = hyperopt.total_tries
|
|
||||||
|
|
||||||
assert current_tries == len(trials.results)
|
|
||||||
assert total_tries == (current_tries + len(trials.results))
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_trials_saves_trials(mocker, init_hyperopt, 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()
|
hyperopt.save_trials()
|
||||||
|
|
||||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has(
|
||||||
'Saving Trials to \'{}\''.format(trials_file),
|
'Saving 1 evaluations to \'{}\''.format(trials_file),
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
mock_dump.assert_called_once()
|
mock_dump.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
|
||||||
trials = create_trials(mocker)
|
trials = create_trials(mocker, hyperopt)
|
||||||
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
|
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
|
||||||
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
|
|
||||||
|
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
hyperopt_trial = hyperopt.read_trials()
|
hyperopt_trial = hyperopt.read_trials()
|
||||||
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -315,11 +137,10 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
|
|||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
assert hyperopt_trial == trials
|
assert hyperopt_trial == trials
|
||||||
mock_open.assert_called_once()
|
|
||||||
mock_load.assert_called_once()
|
mock_load.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
def test_roi_table_generation(init_hyperopt) -> None:
|
def test_roi_table_generation(hyperopt) -> None:
|
||||||
params = {
|
params = {
|
||||||
'roi_t1': 5,
|
'roi_t1': 5,
|
||||||
'roi_t2': 10,
|
'roi_t2': 10,
|
||||||
@ -329,36 +150,35 @@ def test_roi_table_generation(init_hyperopt) -> None:
|
|||||||
'roi_p3': 3,
|
'roi_p3': 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
hyperopt = _HYPEROPT
|
|
||||||
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
|
||||||
|
|
||||||
|
|
||||||
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
|
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
|
||||||
trials = create_trials(mocker)
|
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
|
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
|
||||||
|
parallel = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
|
||||||
|
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
|
||||||
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf.update({'config': 'config.json.example'})
|
||||||
conf.update({'config': 'config.json.example'})
|
default_conf.update({'epochs': 1})
|
||||||
conf.update({'epochs': 1})
|
default_conf.update({'timerange': None})
|
||||||
conf.update({'timerange': None})
|
default_conf.update({'spaces': 'all'})
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
|
|
||||||
hyperopt = Hyperopt(conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
hyperopt.trials = trials
|
|
||||||
hyperopt.tickerdata_to_dataframe = MagicMock()
|
hyperopt.tickerdata_to_dataframe = MagicMock()
|
||||||
|
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
mock_fmin.assert_called_once()
|
parallel.assert_called_once()
|
||||||
|
|
||||||
|
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
|
||||||
|
assert dumper.called
|
||||||
|
|
||||||
|
|
||||||
def test_format_results(init_hyperopt):
|
def test_format_results(hyperopt):
|
||||||
"""
|
|
||||||
Test Hyperopt.format_results()
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Test with BTC as stake_currency
|
# Test with BTC as stake_currency
|
||||||
trades = [
|
trades = [
|
||||||
('ETH/BTC', 2, 2, 123),
|
('ETH/BTC', 2, 2, 123),
|
||||||
@ -368,7 +188,7 @@ def test_format_results(init_hyperopt):
|
|||||||
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
result = _HYPEROPT.format_results(df)
|
result = hyperopt.format_results(df)
|
||||||
assert result.find(' 66.67%')
|
assert result.find(' 66.67%')
|
||||||
assert result.find('Total profit 1.00000000 BTC')
|
assert result.find('Total profit 1.00000000 BTC')
|
||||||
assert result.find('2.0000Σ %')
|
assert result.find('2.0000Σ %')
|
||||||
@ -380,117 +200,61 @@ def test_format_results(init_hyperopt):
|
|||||||
('XPR/EUR', -1, -2, -246)
|
('XPR/EUR', -1, -2, -246)
|
||||||
]
|
]
|
||||||
df = pd.DataFrame.from_records(trades, columns=labels)
|
df = pd.DataFrame.from_records(trades, columns=labels)
|
||||||
result = _HYPEROPT.format_results(df)
|
result = hyperopt.format_results(df)
|
||||||
assert result.find('Total profit 1.00000000 EUR')
|
assert result.find('Total profit 1.00000000 EUR')
|
||||||
|
|
||||||
|
|
||||||
def test_signal_handler(mocker, init_hyperopt):
|
def test_has_space(hyperopt):
|
||||||
"""
|
hyperopt.config.update({'spaces': ['buy', 'roi']})
|
||||||
Test Hyperopt.signal_handler()
|
assert hyperopt.has_space('roi')
|
||||||
"""
|
assert hyperopt.has_space('buy')
|
||||||
m = MagicMock()
|
assert not hyperopt.has_space('stoploss')
|
||||||
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.config.update({'spaces': ['all']})
|
||||||
hyperopt.signal_handler(signal.SIGTERM, None)
|
assert hyperopt.has_space('buy')
|
||||||
assert m.call_count == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_has_space(init_hyperopt):
|
def test_populate_indicators(hyperopt) -> None:
|
||||||
"""
|
|
||||||
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(init_hyperopt) -> None:
|
|
||||||
"""
|
|
||||||
Test Hyperopt.populate_indicators()
|
|
||||||
"""
|
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
||||||
|
|
||||||
# Check if some indicators are generated. We will not test all of them
|
# Check if some indicators are generated. We will not test all of them
|
||||||
assert 'adx' in dataframe
|
assert 'adx' in dataframe
|
||||||
assert 'ao' in dataframe
|
assert 'mfi' in dataframe
|
||||||
assert 'cci' in dataframe
|
assert 'rsi' in dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_buy_strategy_generator(init_hyperopt) -> None:
|
def test_buy_strategy_generator(hyperopt) -> None:
|
||||||
"""
|
|
||||||
Test Hyperopt.buy_strategy_generator()
|
|
||||||
"""
|
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
|
dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
|
||||||
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
|
dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
|
||||||
|
|
||||||
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
|
populate_buy_trend = hyperopt.buy_strategy_generator(
|
||||||
{
|
{
|
||||||
'uptrend_long_ema': {
|
'adx-value': 20,
|
||||||
'enabled': True
|
'fastd-value': 20,
|
||||||
},
|
'mfi-value': 20,
|
||||||
'macd_below_zero': {
|
'rsi-value': 20,
|
||||||
'enabled': True
|
'adx-enabled': True,
|
||||||
},
|
'fastd-enabled': True,
|
||||||
'uptrend_short_ema': {
|
'mfi-enabled': True,
|
||||||
'enabled': True
|
'rsi-enabled': True,
|
||||||
},
|
'trigger': 'bb_lower'
|
||||||
'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)
|
result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
|
||||||
# Check if some indicators are generated. We will not test all of them
|
# Check if some indicators are generated. We will not test all of them
|
||||||
assert 'buy' in result
|
assert 'buy' in result
|
||||||
assert 1 in result['buy']
|
assert 1 in result['buy']
|
||||||
|
|
||||||
|
|
||||||
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
def test_generate_optimizer(mocker, default_conf) -> None:
|
||||||
"""
|
default_conf.update({'config': 'config.json.example'})
|
||||||
Test Hyperopt.generate_optimizer() function
|
default_conf.update({'timerange': None})
|
||||||
"""
|
default_conf.update({'spaces': 'all'})
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf.update({'config': 'config.json.example'})
|
|
||||||
conf.update({'timerange': None})
|
|
||||||
conf.update({'spaces': 'all'})
|
|
||||||
|
|
||||||
trades = [
|
trades = [
|
||||||
('POWR/BTC', 0.023117, 0.000233, 100)
|
('POWR/BTC', 0.023117, 0.000233, 100)
|
||||||
@ -503,35 +267,33 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
|
|||||||
MagicMock(return_value=backtest_result)
|
MagicMock(return_value=backtest_result)
|
||||||
)
|
)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
|
||||||
|
|
||||||
optimizer_param = {
|
optimizer_param = {
|
||||||
'adx': {'enabled': False},
|
'adx-value': 0,
|
||||||
'fastd': {'enabled': True, 'value': 35.0},
|
'fastd-value': 35,
|
||||||
'green_candle': {'enabled': True},
|
'mfi-value': 0,
|
||||||
'macd_below_zero': {'enabled': True},
|
'rsi-value': 0,
|
||||||
'mfi': {'enabled': False},
|
'adx-enabled': False,
|
||||||
'over_sar': {'enabled': False},
|
'fastd-enabled': True,
|
||||||
'roi_p1': 0.01,
|
'mfi-enabled': False,
|
||||||
'roi_p2': 0.01,
|
'rsi-enabled': False,
|
||||||
'roi_p3': 0.1,
|
'trigger': 'macd_cross_signal',
|
||||||
'roi_t1': 60.0,
|
'roi_t1': 60.0,
|
||||||
'roi_t2': 30.0,
|
'roi_t2': 30.0,
|
||||||
'roi_t3': 20.0,
|
'roi_t3': 20.0,
|
||||||
'rsi': {'enabled': False},
|
'roi_p1': 0.01,
|
||||||
|
'roi_p2': 0.01,
|
||||||
|
'roi_p3': 0.1,
|
||||||
'stoploss': -0.4,
|
'stoploss': -0.4,
|
||||||
'trigger': {'type': 'macd_cross_signal'},
|
|
||||||
'uptrend_long_ema': {'enabled': False},
|
|
||||||
'uptrend_short_ema': {'enabled': True},
|
|
||||||
'uptrend_sma': {'enabled': True}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response_expected = {
|
response_expected = {
|
||||||
'loss': 1.9840569076926293,
|
'loss': 1.9840569076926293,
|
||||||
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
|
||||||
'(0.0231Σ%). Avg duration 100.0 mins.',
|
'(0.0231Σ%). Avg duration 100.0 mins.',
|
||||||
'status': 'ok'
|
'params': optimizer_param
|
||||||
}
|
}
|
||||||
|
|
||||||
hyperopt = Hyperopt(conf)
|
hyperopt = Hyperopt(default_conf)
|
||||||
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param)
|
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
|
||||||
assert generate_optimizer_value == response_expected
|
assert generate_optimizer_value == response_expected
|
||||||
|
@ -3,16 +3,19 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import arrow
|
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
from freqtrade import optimize
|
from freqtrade import optimize
|
||||||
from freqtrade.misc import file_dump_json
|
|
||||||
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
|
|
||||||
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
|
|
||||||
load_cached_data_for_updating
|
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
from freqtrade.misc import file_dump_json
|
||||||
|
from freqtrade.optimize.__init__ import (download_backtesting_testdata,
|
||||||
|
download_pairs,
|
||||||
|
load_cached_data_for_updating,
|
||||||
|
load_tickerdata_file,
|
||||||
|
make_testdata_path, trim_tickerlist)
|
||||||
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
|
|
||||||
# Change this if modifying UNITTEST/BTC testdatafile
|
# Change this if modifying UNITTEST/BTC testdatafile
|
||||||
_BTC_UNITTEST_LENGTH = 13681
|
_BTC_UNITTEST_LENGTH = 13681
|
||||||
@ -50,10 +53,7 @@ def _clean_test_file(file: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
"""
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
Test load_data() with 30 min ticker
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
||||||
@ -63,10 +63,7 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) ->
|
|||||||
|
|
||||||
|
|
||||||
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
"""
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
Test load_data() with 5 min ticker
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
|
||||||
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
@ -77,11 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) ->
|
|||||||
|
|
||||||
|
|
||||||
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
"""
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
Test load_data() with 1 min ticker
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
|
||||||
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
@ -94,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co
|
|||||||
"""
|
"""
|
||||||
Test load_data() with 1 min ticker
|
Test load_data() with 1 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
@ -125,7 +118,7 @@ def test_testdata_path() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
@ -268,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=BaseException('File Error'))
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
@ -286,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf)
|
|||||||
|
|
||||||
|
|
||||||
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
# Download a 1 min ticker file
|
||||||
@ -311,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None:
|
|||||||
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||||
]
|
]
|
||||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick)
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=tick)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
|
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
|
||||||
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
||||||
@ -418,10 +411,6 @@ def test_trim_tickerlist() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_file_dump_json() -> None:
|
def test_file_dump_json() -> None:
|
||||||
"""
|
|
||||||
Test file_dump_json()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
|
||||||
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
'test_{id}.json'.format(id=str(uuid.uuid4())))
|
||||||
data = {'bar': 'foo'}
|
data = {'bar': 'foo'}
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for rpc/rpc.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, ANY
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||||
|
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
@ -26,10 +25,6 @@ def prec_satoshi(a, b) -> float:
|
|||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test rpc_trade_status() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -41,6 +36,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -52,31 +48,24 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
rpc._rpc_trade_status()
|
rpc._rpc_trade_status()
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
trades = rpc._rpc_trade_status()
|
results = rpc._rpc_trade_status()
|
||||||
trade = trades[0]
|
|
||||||
|
|
||||||
result_message = [
|
assert {
|
||||||
'*Trade ID:* `1`\n'
|
'trade_id': 1,
|
||||||
'*Current Pair:* '
|
'pair': 'ETH/BTC',
|
||||||
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
'*Open Since:* `just now`\n'
|
'date': ANY,
|
||||||
'*Amount:* `90.99181074`\n'
|
'open_rate': 1.099e-05,
|
||||||
'*Open Rate:* `0.00001099`\n'
|
'close_rate': None,
|
||||||
'*Close Rate:* `None`\n'
|
'current_rate': 1.098e-05,
|
||||||
'*Current Rate:* `0.00001098`\n'
|
'amount': 90.99181074,
|
||||||
'*Close Profit:* `None`\n'
|
'close_profit': None,
|
||||||
'*Current Profit:* `-0.59%`\n'
|
'current_profit': -0.59,
|
||||||
'*Open Order:* `(limit buy rem=0.00000000)`'
|
'open_order': '(limit buy rem=0.00000000)'
|
||||||
]
|
} == results[0]
|
||||||
assert trades == result_message
|
|
||||||
assert trade.find('[ETH/BTC]') >= 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test rpc_status_table() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -88,14 +77,15 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_status_table()
|
rpc._rpc_status_table()
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
|
with pytest.raises(RPCException, match=r'.*no active order*'):
|
||||||
rpc._rpc_status_table()
|
rpc._rpc_status_table()
|
||||||
|
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
@ -107,10 +97,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
|||||||
|
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test rpc_daily_profit() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -122,11 +108,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
# Create some test data
|
# Create some test data
|
||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -159,15 +146,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test rpc_trade_statistics() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -178,10 +162,12 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
with pytest.raises(RPCException, match=r'.*no closed trade*'):
|
||||||
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
|
||||||
@ -236,10 +222,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
|||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||||
"""
|
|
||||||
Test rpc_trade_statistics() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
@ -255,6 +237,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
stake_currency = default_conf['stake_currency']
|
stake_currency = default_conf['stake_currency']
|
||||||
fiat_display_currency = default_conf['fiat_display_currency']
|
fiat_display_currency = default_conf['fiat_display_currency']
|
||||||
|
|
||||||
@ -295,9 +278,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
|||||||
|
|
||||||
|
|
||||||
def test_rpc_balance_handle(default_conf, mocker):
|
def test_rpc_balance_handle(default_conf, mocker):
|
||||||
"""
|
|
||||||
Test rpc_balance() method
|
|
||||||
"""
|
|
||||||
mock_balance = {
|
mock_balance = {
|
||||||
'BTC': {
|
'BTC': {
|
||||||
'free': 10.0,
|
'free': 10.0,
|
||||||
@ -311,12 +291,12 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
patch_coinmarketcap(mocker)
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -325,25 +305,24 @@ def test_rpc_balance_handle(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
|
|
||||||
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
result = rpc._rpc_balance(default_conf['fiat_display_currency'])
|
||||||
assert prec_satoshi(total, 12)
|
assert prec_satoshi(result['total'], 12)
|
||||||
assert prec_satoshi(value, 180000)
|
assert prec_satoshi(result['value'], 180000)
|
||||||
assert 'USD' in symbol
|
assert 'USD' == result['symbol']
|
||||||
assert len(output) == 1
|
assert result['currencies'] == [{
|
||||||
assert 'BTC' in output[0]['currency']
|
'currency': 'BTC',
|
||||||
assert prec_satoshi(output[0]['available'], 10)
|
'available': 10.0,
|
||||||
assert prec_satoshi(output[0]['balance'], 12)
|
'balance': 12.0,
|
||||||
assert prec_satoshi(output[0]['pending'], 2)
|
'pending': 2.0,
|
||||||
assert prec_satoshi(output[0]['est_btc'], 12)
|
'est_btc': 12.0,
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_start(mocker, default_conf) -> None:
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
"""
|
|
||||||
Test rpc_start() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -353,23 +332,20 @@ def test_rpc_start(mocker, default_conf) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
|
|
||||||
result = rpc._rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert '`Starting trader ...`' in result
|
assert {'status': 'starting trader ...'} == result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
result = rpc._rpc_start()
|
result = rpc._rpc_start()
|
||||||
assert '*Status:* `already running`' in result
|
assert {'status': 'already running'} == result
|
||||||
assert freqtradebot.state == State.RUNNING
|
assert freqtradebot.state == State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_stop(mocker, default_conf) -> None:
|
def test_rpc_stop(mocker, default_conf) -> None:
|
||||||
"""
|
|
||||||
Test rpc_stop() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -379,23 +355,21 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
|
|
||||||
result = rpc._rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert '`Stopping trader ...`' in result
|
assert {'status': 'stopping trader ...'} == result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
result = rpc._rpc_stop()
|
result = rpc._rpc_stop()
|
||||||
assert '*Status:* `already stopped`' in result
|
|
||||||
|
assert {'status': 'already stopped'} == result
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
"""
|
|
||||||
Test rpc_forcesell() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
@ -417,14 +391,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_forcesell(None)
|
rpc._rpc_forcesell(None)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
|
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
||||||
rpc._rpc_forcesell(None)
|
rpc._rpc_forcesell(None)
|
||||||
|
|
||||||
rpc._rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
@ -435,10 +410,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
rpc._rpc_forcesell('1')
|
rpc._rpc_forcesell('1')
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_forcesell(None)
|
rpc._rpc_forcesell(None)
|
||||||
|
|
||||||
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_forcesell('all')
|
rpc._rpc_forcesell('all')
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
@ -496,10 +471,6 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
|||||||
|
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, markets, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test rpc_performance() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -512,6 +483,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -535,10 +507,6 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||||
"""
|
|
||||||
Test rpc_count() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -551,6 +519,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
|
|
||||||
trades = rpc._rpc_count()
|
trades = rpc._rpc_count()
|
||||||
|
@ -1,50 +1,31 @@
|
|||||||
"""
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
Unit test file for rpc/rpc_manager.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.rpc.rpc_manager import RPCManager
|
from freqtrade.rpc import RPCMessageType, RPCManager
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_manager_object() -> None:
|
|
||||||
""" Test the Arguments object has the mandatory methods """
|
|
||||||
assert hasattr(RPCManager, 'send_msg')
|
|
||||||
assert hasattr(RPCManager, 'cleanup')
|
|
||||||
|
|
||||||
|
|
||||||
def test__init__(mocker, default_conf) -> None:
|
def test__init__(mocker, default_conf) -> None:
|
||||||
""" Test __init__() method """
|
default_conf['telegram']['enabled'] = False
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
""" Test _init() method with Telegram disabled """
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
conf = deepcopy(default_conf)
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
|
|
||||||
|
|
||||||
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
assert rpc_manager.registered_modules == []
|
assert rpc_manager.registered_modules == []
|
||||||
|
|
||||||
|
|
||||||
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test _init() method with Telegram enabled
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
|
||||||
@ -54,16 +35,11 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test cleanup() method with Telegram disabled
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.cleanup()
|
rpc_manager.cleanup()
|
||||||
|
|
||||||
@ -72,9 +48,6 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test cleanup() method with Telegram enabled
|
|
||||||
"""
|
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
|
||||||
@ -92,32 +65,51 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
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())
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'test'
|
||||||
|
})
|
||||||
|
|
||||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
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())
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
rpc_manager.send_msg('test')
|
rpc_manager.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'test'
|
||||||
|
})
|
||||||
|
|
||||||
assert log_has('Sending rpc message: test', caplog.record_tuples)
|
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
|
||||||
assert telegram_mock.call_count == 1
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
default_conf['webhook'] = {'enabled': False}
|
||||||
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
|
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||||
|
assert rpc_manager.registered_modules == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||||
|
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
|
||||||
|
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
|
||||||
|
assert len(rpc_manager.registered_modules) == 1
|
||||||
|
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
# pragma pylint: disable=protected-access, unused-argument, invalid-name
|
# pragma pylint: disable=protected-access, unused-argument, invalid-name
|
||||||
# pragma pylint: disable=too-many-lines, too-many-arguments
|
# pragma pylint: disable=too-many-lines, too-many-arguments
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for rpc/telegram.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from copy import deepcopy
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, ANY
|
||||||
|
|
||||||
from telegram import Update, Message, Chat
|
import arrow
|
||||||
|
import pytest
|
||||||
|
from telegram import Chat, Message, Update
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.telegram import Telegram
|
from freqtrade.rpc import RPCMessageType
|
||||||
from freqtrade.rpc.telegram import authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has
|
from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
|
patch_exchange)
|
||||||
|
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||||
|
from freqtrade.tests.conftest import patch_coinmarketcap
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
@ -51,9 +51,6 @@ class DummyCls(Telegram):
|
|||||||
|
|
||||||
|
|
||||||
def test__init__(default_conf, mocker) -> None:
|
def test__init__(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test __init__() method
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
|
||||||
@ -63,7 +60,6 @@ def test__init__(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog) -> None:
|
def test_init(default_conf, mocker, caplog) -> None:
|
||||||
""" Test _init() method """
|
|
||||||
start_polling = MagicMock()
|
start_polling = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||||
|
|
||||||
@ -82,9 +78,6 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_cleanup(default_conf, mocker) -> None:
|
def test_cleanup(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test cleanup() method
|
|
||||||
"""
|
|
||||||
updater_mock = MagicMock()
|
updater_mock = MagicMock()
|
||||||
updater_mock.stop = MagicMock()
|
updater_mock.stop = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
|
||||||
@ -95,10 +88,6 @@ def test_cleanup(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
def test_authorized_only(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test authorized_only() method when we are authorized
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
patch_exchange(mocker, None)
|
patch_exchange(mocker, None)
|
||||||
|
|
||||||
@ -106,9 +95,10 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
|||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['telegram']['enabled'] = False
|
||||||
conf['telegram']['enabled'] = False
|
bot = FreqtradeBot(default_conf)
|
||||||
dummy = DummyCls(FreqtradeBot(conf))
|
patch_get_signal(bot, (True, False))
|
||||||
|
dummy = DummyCls(bot)
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is True
|
assert dummy.state['called'] is True
|
||||||
assert log_has(
|
assert log_has(
|
||||||
@ -126,19 +116,16 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test authorized_only() method when we are unauthorized
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
patch_exchange(mocker, None)
|
patch_exchange(mocker, None)
|
||||||
chat = Chat(0xdeadbeef, 0)
|
chat = Chat(0xdeadbeef, 0)
|
||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['telegram']['enabled'] = False
|
||||||
conf['telegram']['enabled'] = False
|
bot = FreqtradeBot(default_conf)
|
||||||
dummy = DummyCls(FreqtradeBot(conf))
|
patch_get_signal(bot, (True, False))
|
||||||
|
dummy = DummyCls(bot)
|
||||||
dummy.dummy_handler(bot=MagicMock(), update=update)
|
dummy.dummy_handler(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -156,19 +143,18 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test authorized_only() method when an exception is thrown
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
update = Update(randint(1, 100))
|
update = Update(randint(1, 100))
|
||||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['telegram']['enabled'] = False
|
||||||
conf['telegram']['enabled'] = False
|
|
||||||
dummy = DummyCls(FreqtradeBot(conf))
|
bot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(bot, (True, False))
|
||||||
|
dummy = DummyCls(bot)
|
||||||
|
|
||||||
dummy.dummy_exception(bot=MagicMock(), update=update)
|
dummy.dummy_exception(bot=MagicMock(), update=update)
|
||||||
assert dummy.state['called'] is False
|
assert dummy.state['called'] is False
|
||||||
assert not log_has(
|
assert not log_has(
|
||||||
@ -186,16 +172,12 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
"""
|
|
||||||
Test _status() method
|
|
||||||
"""
|
|
||||||
update.message.chat.id = 123
|
update.message.chat.id = 123
|
||||||
conf = deepcopy(default_conf)
|
default_conf['telegram']['enabled'] = False
|
||||||
conf['telegram']['enabled'] = False
|
default_conf['telegram']['chat_id'] = 123
|
||||||
conf['telegram']['chat_id'] = 123
|
|
||||||
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -209,13 +191,26 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
|
_rpc_trade_status=MagicMock(return_value=[{
|
||||||
|
'trade_id': 1,
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'date': arrow.utcnow(),
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'close_rate': None,
|
||||||
|
'current_rate': 1.098e-05,
|
||||||
|
'amount': 90.99181074,
|
||||||
|
'close_profit': None,
|
||||||
|
'current_profit': -0.59,
|
||||||
|
'open_order': '(limit buy rem=0.00000000)'
|
||||||
|
}]),
|
||||||
_status_table=status_table,
|
_status_table=status_table,
|
||||||
_send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -223,7 +218,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
|||||||
freqtradebot.create_trade()
|
freqtradebot.create_trade()
|
||||||
|
|
||||||
telegram._status(bot=MagicMock(), update=update)
|
telegram._status(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 3
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
update.message.text = MagicMock()
|
update.message.text = MagicMock()
|
||||||
update.message.text.replace = MagicMock(return_value='table 2 3')
|
update.message.text.replace = MagicMock(return_value='table 2 3')
|
||||||
@ -232,10 +227,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _status() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -255,6 +246,8 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
|||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -279,10 +272,6 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
|||||||
|
|
||||||
|
|
||||||
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _status_table() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -300,9 +289,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
conf = deepcopy(default_conf)
|
default_conf['stake_amount'] = 15.0
|
||||||
conf['stake_amount'] = 15.0
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
freqtradebot = FreqtradeBot(conf)
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -333,13 +323,9 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
|||||||
|
|
||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, markets, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _daily() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.fiat_convert.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=15000.0
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -358,6 +344,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -407,10 +394,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _daily() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -426,6 +409,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Try invalid data
|
# Try invalid data
|
||||||
@ -446,12 +430,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
|||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _profit() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -468,6 +448,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._profit(bot=MagicMock(), update=update)
|
telegram._profit(bot=MagicMock(), update=update)
|
||||||
@ -507,10 +488,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _balance() method
|
|
||||||
"""
|
|
||||||
|
|
||||||
mock_balance = {
|
mock_balance = {
|
||||||
'BTC': {
|
'BTC': {
|
||||||
'total': 12.0,
|
'total': 12.0,
|
||||||
@ -535,9 +512,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def mock_ticker(symbol, refresh):
|
def mock_ticker(symbol, refresh):
|
||||||
"""
|
|
||||||
Mock Bittrex.get_ticker() response
|
|
||||||
"""
|
|
||||||
if symbol == 'BTC/USDT':
|
if symbol == 'BTC/USDT':
|
||||||
return {
|
return {
|
||||||
'bid': 10000.00,
|
'bid': 10000.00,
|
||||||
@ -551,7 +525,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
'last': 0.1,
|
'last': 0.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
|
||||||
@ -564,6 +537,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
@ -577,11 +552,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
assert 'BTC: 14.00000000' in result
|
assert 'BTC: 14.00000000' in result
|
||||||
|
|
||||||
|
|
||||||
def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _balance() method when the Exchange platform returns nothing
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
|
||||||
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -592,18 +563,17 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._balance(bot=MagicMock(), update=update)
|
telegram._balance(bot=MagicMock(), update=update)
|
||||||
result = msg_mock.call_args_list[0][0][0]
|
result = msg_mock.call_args_list[0][0][0]
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '`All balances are zero.`' in result
|
assert 'all balances are zero' in result
|
||||||
|
|
||||||
|
|
||||||
def test_start_handle(default_conf, update, mocker) -> None:
|
def test_start_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _start() method
|
|
||||||
"""
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
@ -622,9 +592,6 @@ def test_start_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _start() method
|
|
||||||
"""
|
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
@ -644,9 +611,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_stop_handle(default_conf, update, mocker) -> None:
|
def test_stop_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _stop() method
|
|
||||||
"""
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -663,13 +627,10 @@ def test_stop_handle(default_conf, update, mocker) -> None:
|
|||||||
telegram._stop(bot=MagicMock(), update=update)
|
telegram._stop(bot=MagicMock(), update=update)
|
||||||
assert freqtradebot.state == State.STOPPED
|
assert freqtradebot.state == State.STOPPED
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _stop() method
|
|
||||||
"""
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -690,7 +651,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
||||||
""" Test _reload_conf() method """
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -707,17 +667,13 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
|
|||||||
telegram._reload_conf(bot=MagicMock(), update=update)
|
telegram._reload_conf(bot=MagicMock(), update=update)
|
||||||
assert freqtradebot.state == State.RELOAD_CONF
|
assert freqtradebot.state == State.RELOAD_CONF
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
|
assert 'reloading config' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, fee,
|
def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||||
ticker_sell_up, markets, mocker) -> None:
|
ticker_sell_up, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _forcesell() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -729,6 +685,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -744,20 +701,26 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
assert {
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
'exchange': 'Bittrex',
|
||||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
'pair': 'ETH/BTC',
|
||||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
'gain': 'profit',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'limit': 1.172e-05,
|
||||||
|
'amount': 90.99181073703367,
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'current_rate': 1.172e-05,
|
||||||
|
'profit_amount': 6.126e-05,
|
||||||
|
'profit_percent': 0.06110514,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD',
|
||||||
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
ticker_sell_down, markets, mocker) -> None:
|
ticker_sell_down, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _forcesell() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
@ -771,6 +734,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -790,19 +754,26 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
|
|
||||||
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
|
assert {
|
||||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
'exchange': 'Bittrex',
|
||||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
'pair': 'ETH/BTC',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'limit': 1.044e-05,
|
||||||
|
'amount': 90.99181073703367,
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'current_rate': 1.044e-05,
|
||||||
|
'profit_amount': -5.492e-05,
|
||||||
|
'profit_percent': -0.05478343,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD',
|
||||||
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _forcesell() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
@ -817,6 +788,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -828,17 +800,25 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
|||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert rpc_mock.call_count == 4
|
assert rpc_mock.call_count == 4
|
||||||
for args in rpc_mock.call_args_list:
|
msg = rpc_mock.call_args_list[0][0][0]
|
||||||
assert '0.00001098' in args[0][0]
|
assert {
|
||||||
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
assert '-0.089 USD' in args[0][0]
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': ANY,
|
||||||
|
'limit': 1.098e-05,
|
||||||
|
'amount': 90.99181073703367,
|
||||||
|
'open_rate': 1.099e-05,
|
||||||
|
'current_rate': 1.098e-05,
|
||||||
|
'profit_amount': -5.91e-06,
|
||||||
|
'profit_percent': -0.00589292,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD',
|
||||||
|
} == msg
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _forcesell() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -850,6 +830,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -865,7 +846,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
update.message.text = '/forcesell'
|
update.message.text = '/forcesell'
|
||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
|
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
# Invalid argument
|
# Invalid argument
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
@ -873,15 +854,11 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
update.message.text = '/forcesell 123456'
|
update.message.text = '/forcesell 123456'
|
||||||
telegram._forcesell(bot=MagicMock(), update=update)
|
telegram._forcesell(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
|
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, update, ticker, fee,
|
def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _performance() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -898,6 +875,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
@ -920,10 +898,6 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _performance() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -933,6 +907,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
# Trader is not running
|
# Trader is not running
|
||||||
@ -943,10 +918,6 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _count() method
|
|
||||||
"""
|
|
||||||
patch_get_signal(mocker, (True, False))
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -963,6 +934,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
|||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
@ -987,9 +959,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
|||||||
|
|
||||||
|
|
||||||
def test_help_handle(default_conf, update, mocker) -> None:
|
def test_help_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _help() method
|
|
||||||
"""
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1007,9 +976,6 @@ def test_help_handle(default_conf, update, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_version_handle(default_conf, update, mocker) -> None:
|
def test_version_handle(default_conf, update, mocker) -> None:
|
||||||
"""
|
|
||||||
Test _version() method
|
|
||||||
"""
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1025,15 +991,224 @@ def test_version_handle(default_conf, update, mocker) -> None:
|
|||||||
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg(default_conf, mocker) -> None:
|
def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||||
"""
|
msg_mock = MagicMock()
|
||||||
Test send_msg() method
|
mocker.patch.multiple(
|
||||||
"""
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'limit': 1.099e-05,
|
||||||
|
'stake_amount': 0.001,
|
||||||
|
'stake_amount_fiat': 0.0,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'USD'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] \
|
||||||
|
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||||
|
'with limit `0.00001099\n' \
|
||||||
|
'(0.001000 BTC,0.000 USD)`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
old_convamount = telegram._fiat_converter.convert_amount
|
||||||
|
telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'KEY/ETH',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||||
|
'limit': 3.201e-05,
|
||||||
|
'amount': 1333.3333333333335,
|
||||||
|
'open_rate': 7.5e-05,
|
||||||
|
'current_rate': 3.201e-05,
|
||||||
|
'profit_amount': -0.05746268,
|
||||||
|
'profit_percent': -0.57405275,
|
||||||
|
'stake_currency': 'ETH',
|
||||||
|
'fiat_currency': 'USD'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] \
|
||||||
|
== '*Binance:* Selling [KEY/ETH]' \
|
||||||
|
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||||
|
'*Limit:* `0.00003201`\n' \
|
||||||
|
'*Amount:* `1333.33333333`\n' \
|
||||||
|
'*Open Rate:* `0.00007500`\n' \
|
||||||
|
'*Current Rate:* `0.00003201`\n' \
|
||||||
|
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`'
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'KEY/ETH',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||||
|
'limit': 3.201e-05,
|
||||||
|
'amount': 1333.3333333333335,
|
||||||
|
'open_rate': 7.5e-05,
|
||||||
|
'current_rate': 3.201e-05,
|
||||||
|
'profit_amount': -0.05746268,
|
||||||
|
'profit_percent': -0.57405275,
|
||||||
|
'stake_currency': 'ETH',
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] \
|
||||||
|
== '*Binance:* Selling [KEY/ETH]' \
|
||||||
|
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||||
|
'*Limit:* `0.00003201`\n' \
|
||||||
|
'*Amount:* `1333.33333333`\n' \
|
||||||
|
'*Open Rate:* `0.00007500`\n' \
|
||||||
|
'*Current Rate:* `0.00003201`\n' \
|
||||||
|
'*Profit:* `-57.41%`'
|
||||||
|
# Reset singleton function to avoid random breaks
|
||||||
|
telegram._fiat_converter.convert_amount = old_convamount
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_status_notification(default_conf, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'running'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] == '*Status:* `running`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_warning_notification(default_conf, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
|
'status': 'message'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_notification(default_conf, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.CUSTOM_NOTIFICATION,
|
||||||
|
'status': '*Custom:* `Hello World`'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_unknown_type(default_conf, mocker) -> None:
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': None,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
|
del default_conf['fiat_display_currency']
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||||
|
'limit': 1.099e-05,
|
||||||
|
'stake_amount': 0.001,
|
||||||
|
'stake_amount_fiat': 0.0,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': None
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] \
|
||||||
|
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||||
|
'with limit `0.00001099\n' \
|
||||||
|
'(0.001000 BTC)`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||||
|
del default_conf['fiat_display_currency']
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'exchange': 'Binance',
|
||||||
|
'pair': 'KEY/ETH',
|
||||||
|
'gain': 'loss',
|
||||||
|
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||||
|
'limit': 3.201e-05,
|
||||||
|
'amount': 1333.3333333333335,
|
||||||
|
'open_rate': 7.5e-05,
|
||||||
|
'current_rate': 3.201e-05,
|
||||||
|
'profit_amount': -0.05746268,
|
||||||
|
'profit_percent': -0.57405275,
|
||||||
|
'stake_currency': 'ETH',
|
||||||
|
'fiat_currency': 'USD'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] \
|
||||||
|
== '*Binance:* Selling [KEY/ETH]' \
|
||||||
|
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||||
|
'*Limit:* `0.00003201`\n' \
|
||||||
|
'*Amount:* `1333.33333333`\n' \
|
||||||
|
'*Open Rate:* `0.00007500`\n' \
|
||||||
|
'*Current Rate:* `0.00003201`\n' \
|
||||||
|
'*Profit:* `-57.41%`'
|
||||||
|
|
||||||
|
|
||||||
|
def test__send_msg(default_conf, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
@ -1041,16 +1216,12 @@ def test_send_msg(default_conf, mocker) -> None:
|
|||||||
assert len(bot.method_calls) == 1
|
assert len(bot.method_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
|
def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test send_msg() method
|
|
||||||
"""
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
|
||||||
telegram._config['telegram']['enabled'] = True
|
telegram._config['telegram']['enabled'] = True
|
||||||
|
166
freqtrade/tests/rpc/test_rpc_webhook.py
Normal file
166
freqtrade/tests/rpc/test_rpc_webhook.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103, protected-access
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
from freqtrade.rpc import RPCMessageType
|
||||||
|
from freqtrade.rpc.webhook import Webhook
|
||||||
|
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
|
||||||
|
|
||||||
|
|
||||||
|
def get_webhook_dict() -> dict:
|
||||||
|
return {
|
||||||
|
"enabled": True,
|
||||||
|
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
|
||||||
|
"webhookbuy": {
|
||||||
|
"value1": "Buying {pair}",
|
||||||
|
"value2": "limit {limit:8f}",
|
||||||
|
"value3": "{stake_amount:8f} {stake_currency}"
|
||||||
|
},
|
||||||
|
"webhooksell": {
|
||||||
|
"value1": "Selling {pair}",
|
||||||
|
"value2": "limit {limit:8f}",
|
||||||
|
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||||
|
},
|
||||||
|
"webhookstatus": {
|
||||||
|
"value1": "Status: {status}",
|
||||||
|
"value2": "",
|
||||||
|
"value3": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test__init__(mocker, default_conf):
|
||||||
|
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
|
||||||
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
assert webhook._config == default_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg(default_conf, mocker):
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': "http://mockedurl/ETH_BTC",
|
||||||
|
'limit': 0.005,
|
||||||
|
'stake_amount': 0.8,
|
||||||
|
'stake_amount_fiat': 500,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'EUR'
|
||||||
|
}
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
webhook.send_msg(msg=msg)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||||
|
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||||
|
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||||
|
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
||||||
|
# Test sell
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'gain': "profit",
|
||||||
|
'market_url': "http://mockedurl/ETH_BTC",
|
||||||
|
'limit': 0.005,
|
||||||
|
'amount': 0.8,
|
||||||
|
'open_rate': 0.004,
|
||||||
|
'current_rate': 0.005,
|
||||||
|
'profit_amount': 0.001,
|
||||||
|
'profit_percent': 0.20,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
}
|
||||||
|
webhook.send_msg(msg=msg)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||||
|
default_conf["webhook"]["webhooksell"]["value1"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||||
|
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||||
|
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
||||||
|
|
||||||
|
# Test notification
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||||
|
'status': 'Unfilled sell order for BTC cancelled due to timeout'
|
||||||
|
}
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
webhook.send_msg(msg)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||||
|
default_conf["webhook"]["webhookstatus"]["value1"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||||
|
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg))
|
||||||
|
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||||
|
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg))
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_send_msg(default_conf, mocker, caplog):
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
default_conf["webhook"]["webhookbuy"] = None
|
||||||
|
|
||||||
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
|
||||||
|
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
msg = {
|
||||||
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
|
'exchange': 'Bittrex',
|
||||||
|
'pair': 'ETH/BTC',
|
||||||
|
'market_url': "http://mockedurl/ETH_BTC",
|
||||||
|
'limit': 0.005,
|
||||||
|
'stake_amount': 0.8,
|
||||||
|
'stake_amount_fiat': 500,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'fiat_currency': 'EUR'
|
||||||
|
}
|
||||||
|
webhook.send_msg(msg)
|
||||||
|
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
|
||||||
|
"Exception: 'DEADBEEF'", caplog.record_tuples)
|
||||||
|
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||||
|
msg = {
|
||||||
|
'type': 'DEADBEEF',
|
||||||
|
'status': 'whatever'
|
||||||
|
}
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
webhook.send_msg(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def test__send_msg(default_conf, mocker, caplog):
|
||||||
|
default_conf["webhook"] = get_webhook_dict()
|
||||||
|
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||||
|
msg = {'value1': 'DEADBEEF',
|
||||||
|
'value2': 'ALIVEBEEF',
|
||||||
|
'value3': 'FREQTRADE'}
|
||||||
|
post = MagicMock()
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||||
|
webhook._send_msg(msg)
|
||||||
|
|
||||||
|
assert post.call_count == 1
|
||||||
|
assert post.call_args[1] == {'data': msg}
|
||||||
|
assert post.call_args[0] == (default_conf['webhook']['url'], )
|
||||||
|
|
||||||
|
post = MagicMock(side_effect=RequestException)
|
||||||
|
mocker.patch("freqtrade.rpc.webhook.post", post)
|
||||||
|
webhook._send_msg(msg)
|
||||||
|
assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples)
|
235
freqtrade/tests/strategy/legacy_strategy.py
Normal file
235
freqtrade/tests/strategy/legacy_strategy.py
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
|
||||||
|
# --- Do not remove these libs ---
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
from pandas import DataFrame
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
# Add your lib to import here
|
||||||
|
import talib.abstract as ta
|
||||||
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
import numpy # noqa
|
||||||
|
|
||||||
|
|
||||||
|
# This class is a sample. Feel free to customize it.
|
||||||
|
class TestStrategyLegacy(IStrategy):
|
||||||
|
"""
|
||||||
|
This is a test strategy using the legacy function headers, which will be
|
||||||
|
removed in a future update.
|
||||||
|
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
|
||||||
|
for a uptodate version of this template.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
minimal_roi = {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optimal stoploss designed for the strategy
|
||||||
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
|
stoploss = -0.10
|
||||||
|
|
||||||
|
# Optimal ticker interval for the strategy
|
||||||
|
ticker_interval = '5m'
|
||||||
|
|
||||||
|
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)
|
||||||
|
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']
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Overlap Studies
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
|
# 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['adx'] > 30) &
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1))
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe['adx'] > 70) &
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1))
|
||||||
|
),
|
||||||
|
'sell'] = 1
|
||||||
|
return dataframe
|
@ -3,14 +3,14 @@ import json
|
|||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
|
||||||
return Analyze.parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
def test_default_strategy_structure():
|
def test_default_strategy_structure():
|
||||||
@ -23,12 +23,13 @@ def test_default_strategy_structure():
|
|||||||
|
|
||||||
|
|
||||||
def test_default_strategy(result):
|
def test_default_strategy(result):
|
||||||
strategy = DefaultStrategy()
|
strategy = DefaultStrategy({})
|
||||||
|
|
||||||
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
assert type(strategy.minimal_roi) is dict
|
assert type(strategy.minimal_roi) is dict
|
||||||
assert type(strategy.stoploss) is float
|
assert type(strategy.stoploss) is float
|
||||||
assert type(strategy.ticker_interval) is str
|
assert type(strategy.ticker_interval) is str
|
||||||
indicators = strategy.populate_indicators(result)
|
indicators = strategy.populate_indicators(result, metadata)
|
||||||
assert type(indicators) is DataFrame
|
assert type(indicators) is DataFrame
|
||||||
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
|
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||||
assert type(strategy.populate_sell_trend(indicators)) is DataFrame
|
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
||||||
|
107
freqtrade/tests/strategy/test_interface.py
Normal file
107
freqtrade/tests/strategy/test_interface.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
# Avoid to reinit the same object again and again
|
||||||
|
_STRATEGY = DefaultStrategy(config={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_latest_buy_signal(mocker, default_conf):
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
||||||
|
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_latest_sell_signal(mocker, default_conf):
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
|
||||||
|
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
||||||
|
)
|
||||||
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_empty(default_conf, mocker, caplog):
|
||||||
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
|
||||||
|
None)
|
||||||
|
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.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
side_effect=ValueError('xyz')
|
||||||
|
)
|
||||||
|
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], 1)
|
||||||
|
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.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame([])
|
||||||
|
)
|
||||||
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
||||||
|
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)
|
||||||
|
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
||||||
|
# this is necessary as the last candle is removed (partial candles) by default
|
||||||
|
oldtime = arrow.utcnow().shift(minutes=-16)
|
||||||
|
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
return_value=DataFrame(ticks)
|
||||||
|
)
|
||||||
|
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
|
||||||
|
assert log_has(
|
||||||
|
'Outdated history for pair xyz. Last tick is 16 minutes old',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=MagicMock())
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
mocker.patch.object(
|
||||||
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
side_effect=Exception('invalid ticker history ')
|
||||||
|
)
|
||||||
|
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
|
||||||
|
timerange = TimeRange(None, 'line', 0, -100)
|
||||||
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
||||||
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
|
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
@ -1,8 +1,11 @@
|
|||||||
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
# pragma pylint: disable=missing-docstring, protected-access, C0103
|
||||||
import logging
|
import logging
|
||||||
import os
|
from base64 import urlsafe_b64encode
|
||||||
|
from os import path
|
||||||
|
import warnings
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.strategy import import_strategy
|
from freqtrade.strategy import import_strategy
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
@ -12,14 +15,15 @@ from freqtrade.strategy.resolver import StrategyResolver
|
|||||||
|
|
||||||
def test_import_strategy(caplog):
|
def test_import_strategy(caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
default_config = {}
|
||||||
|
|
||||||
strategy = DefaultStrategy()
|
strategy = DefaultStrategy(default_config)
|
||||||
strategy.some_method = lambda *args, **kwargs: 42
|
strategy.some_method = lambda *args, **kwargs: 42
|
||||||
|
|
||||||
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
|
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
|
||||||
assert strategy.some_method() == 42
|
assert strategy.some_method() == 42
|
||||||
|
|
||||||
imported_strategy = import_strategy(strategy)
|
imported_strategy = import_strategy(strategy, default_config)
|
||||||
|
|
||||||
assert dir(strategy) == dir(imported_strategy)
|
assert dir(strategy) == dir(imported_strategy)
|
||||||
|
|
||||||
@ -35,25 +39,42 @@ def test_import_strategy(caplog):
|
|||||||
|
|
||||||
|
|
||||||
def test_search_strategy():
|
def test_search_strategy():
|
||||||
default_location = os.path.join(os.path.dirname(
|
default_config = {}
|
||||||
os.path.realpath(__file__)), '..', '..', 'strategy'
|
default_location = path.join(path.dirname(
|
||||||
|
path.realpath(__file__)), '..', '..', 'strategy'
|
||||||
)
|
)
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy
|
StrategyResolver._search_strategy(
|
||||||
|
default_location,
|
||||||
|
config=default_config,
|
||||||
|
strategy_name='DefaultStrategy'
|
||||||
|
),
|
||||||
|
IStrategy
|
||||||
)
|
)
|
||||||
assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None
|
assert StrategyResolver._search_strategy(
|
||||||
|
default_location,
|
||||||
|
config=default_config,
|
||||||
|
strategy_name='NotFoundStrategy'
|
||||||
|
) is None
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy(result):
|
def test_load_strategy(result):
|
||||||
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
resolver = StrategyResolver({'strategy': 'TestStrategy'})
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_strategy_byte64(result):
|
||||||
|
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
|
||||||
|
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
|
||||||
|
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
|
||||||
|
assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_load_strategy_invalid_directory(result, caplog):
|
def test_load_strategy_invalid_directory(result, caplog):
|
||||||
resolver = StrategyResolver()
|
resolver = StrategyResolver()
|
||||||
extra_dir = os.path.join('some', 'path')
|
extra_dir = path.join('some', 'path')
|
||||||
resolver._load_strategy('TestStrategy', extra_dir)
|
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
'freqtrade.strategy.resolver',
|
'freqtrade.strategy.resolver',
|
||||||
@ -61,8 +82,7 @@ def test_load_strategy_invalid_directory(result, caplog):
|
|||||||
'Path "{}" does not exist'.format(extra_dir),
|
'Path "{}" does not exist'.format(extra_dir),
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_not_found_strategy():
|
def test_load_not_found_strategy():
|
||||||
@ -70,27 +90,30 @@ def test_load_not_found_strategy():
|
|||||||
with pytest.raises(ImportError,
|
with pytest.raises(ImportError,
|
||||||
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
|
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
|
||||||
r' This class does not exist or contains Python code errors'):
|
r' This class does not exist or contains Python code errors'):
|
||||||
strategy._load_strategy('NotFoundStrategy')
|
strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
|
||||||
|
|
||||||
|
|
||||||
def test_strategy(result):
|
def test_strategy(result):
|
||||||
resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
|
config = {'strategy': 'DefaultStrategy'}
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'minimal_roi')
|
resolver = StrategyResolver(config)
|
||||||
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.04
|
assert resolver.strategy.minimal_roi[0] == 0.04
|
||||||
|
assert config["minimal_roi"]['0'] == 0.04
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'stoploss')
|
|
||||||
assert resolver.strategy.stoploss == -0.10
|
assert resolver.strategy.stoploss == -0.10
|
||||||
|
assert config['stoploss'] == -0.10
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_indicators')
|
assert resolver.strategy.ticker_interval == '5m'
|
||||||
assert 'adx' in resolver.strategy.populate_indicators(result)
|
assert config['ticker_interval'] == '5m'
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_buy_trend')
|
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result))
|
assert 'adx' in df_indicators
|
||||||
|
|
||||||
|
dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'populate_sell_trend')
|
dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
|
||||||
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
|
|
||||||
assert 'sell' in dataframe.columns
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
@ -104,7 +127,6 @@ def test_strategy_override_minimal_roi(caplog):
|
|||||||
}
|
}
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'minimal_roi')
|
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
@ -120,7 +142,6 @@ def test_strategy_override_stoploss(caplog):
|
|||||||
}
|
}
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'stoploss')
|
|
||||||
assert resolver.strategy.stoploss == -0.5
|
assert resolver.strategy.stoploss == -0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
@ -137,9 +158,64 @@ def test_strategy_override_ticker_interval(caplog):
|
|||||||
}
|
}
|
||||||
resolver = StrategyResolver(config)
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
assert hasattr(resolver.strategy, 'ticker_interval')
|
|
||||||
assert resolver.strategy.ticker_interval == 60
|
assert resolver.strategy.ticker_interval == 60
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecate_populate_indicators(result):
|
||||||
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
|
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||||
|
'strategy_path': default_location})
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
# Cause all warnings to always be triggered.
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
|
||||||
|
assert len(w) == 1
|
||||||
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
in str(w[-1].message)
|
||||||
|
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
# Cause all warnings to always be triggered.
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
|
||||||
|
assert len(w) == 1
|
||||||
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
in str(w[-1].message)
|
||||||
|
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
# Cause all warnings to always be triggered.
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
|
||||||
|
assert len(w) == 1
|
||||||
|
assert issubclass(w[-1].category, DeprecationWarning)
|
||||||
|
assert "deprecated - check out the Sample strategy to see the current function headers!" \
|
||||||
|
in str(w[-1].message)
|
||||||
|
|
||||||
|
|
||||||
|
def test_call_deprecated_function(result, monkeypatch):
|
||||||
|
default_location = path.join(path.dirname(path.realpath(__file__)))
|
||||||
|
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
|
||||||
|
'strategy_path': default_location})
|
||||||
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
|
|
||||||
|
# Make sure we are using a legacy function
|
||||||
|
assert resolver.strategy._populate_fun_len == 2
|
||||||
|
assert resolver.strategy._buy_fun_len == 2
|
||||||
|
assert resolver.strategy._sell_fun_len == 2
|
||||||
|
|
||||||
|
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
|
||||||
|
assert type(indicator_df) is DataFrame
|
||||||
|
assert 'adx' in indicator_df.columns
|
||||||
|
|
||||||
|
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
|
||||||
|
assert type(buydf) is DataFrame
|
||||||
|
assert 'buy' in buydf.columns
|
||||||
|
|
||||||
|
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
|
||||||
|
assert type(selldf) is DataFrame
|
||||||
|
assert 'sell' in selldf
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
# pragma pylint: disable=missing-docstring,C0103,protected-access
|
||||||
|
|
||||||
import freqtrade.tests.conftest as tt # test tools
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
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
|
||||||
# perhaps try to anticipate that by using some python package
|
# perhaps try to anticipate that by using some python package
|
||||||
@ -10,7 +11,6 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
def whitelist_conf():
|
def whitelist_conf():
|
||||||
config = tt.default_conf()
|
config = tt.default_conf()
|
||||||
|
|
||||||
config['stake_currency'] = 'BTC'
|
config['stake_currency'] = 'BTC'
|
||||||
config['exchange']['pair_whitelist'] = [
|
config['exchange']['pair_whitelist'] = [
|
||||||
'ETH/BTC',
|
'ETH/BTC',
|
||||||
@ -19,7 +19,6 @@ def whitelist_conf():
|
|||||||
'SWT/BTC',
|
'SWT/BTC',
|
||||||
'BCC/BTC'
|
'BCC/BTC'
|
||||||
]
|
]
|
||||||
|
|
||||||
config['exchange']['pair_blacklist'] = [
|
config['exchange']['pair_blacklist'] = [
|
||||||
'BLK/BTC'
|
'BLK/BTC'
|
||||||
]
|
]
|
||||||
|
@ -1,197 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for analyse.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze, SignalType
|
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
|
||||||
from freqtrade.arguments import TimeRange
|
|
||||||
from freqtrade.tests.conftest import log_has, get_patched_exchange
|
|
||||||
|
|
||||||
# Avoid to reinit the same object again and again
|
|
||||||
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
|
|
||||||
|
|
||||||
|
|
||||||
def test_signaltype_object() -> None:
|
|
||||||
"""
|
|
||||||
Test the SignalType object has the mandatory Constants
|
|
||||||
: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) - 1 == len(dataframe.index) # last partial candle removed
|
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_columns(result):
|
|
||||||
assert result.columns.tolist() == \
|
|
||||||
['date', 'open', 'high', 'low', 'close', 'volume']
|
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(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
|
|
||||||
|
|
||||||
|
|
||||||
def test_populates_sell_trend(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
|
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker, default_conf):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (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(exchange, 'ETH/BTC', '5m') == (False, True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_sell_signal(mocker, default_conf):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
|
|
||||||
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty(default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
|
||||||
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
side_effect=ValueError('xyz')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
|
|
||||||
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
|
|
||||||
caplog.set_level(logging.INFO)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
return_value=DataFrame([])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', 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.exchange.Exchange.get_ticker_history', return_value=1)
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
|
|
||||||
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(exchange, 'xyz', default_conf['ticker_interval'])
|
|
||||||
assert log_has(
|
|
||||||
'Outdated history for pair xyz. Last tick is 11 minutes old',
|
|
||||||
caplog.record_tuples
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.analyze.Analyze',
|
|
||||||
analyze_ticker=MagicMock(
|
|
||||||
side_effect=Exception('invalid ticker history ')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_ticker_dataframe(ticker_history):
|
|
||||||
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|
||||||
|
|
||||||
# Test file with BV data
|
|
||||||
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
|
|
||||||
assert dataframe.columns.tolist() == columns
|
|
||||||
|
|
||||||
|
|
||||||
def test_tickerdata_to_dataframe(default_conf) -> None:
|
|
||||||
"""
|
|
||||||
Test Analyze.tickerdata_to_dataframe() method
|
|
||||||
"""
|
|
||||||
analyze = Analyze(default_conf)
|
|
||||||
|
|
||||||
timerange = TimeRange(None, 'line', 0, -100)
|
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
|
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
|
||||||
data = analyze.tickerdata_to_dataframe(tickerlist)
|
|
||||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
|
@ -1,41 +1,24 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for arguments.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments, TimeRange
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
|
|
||||||
|
|
||||||
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
|
# Parse common command-line-arguments. Used for all tools
|
||||||
def test_parse_args_none() -> None:
|
def test_parse_args_none() -> None:
|
||||||
arguments = Arguments([], '')
|
arguments = Arguments([], '')
|
||||||
assert isinstance(arguments, Arguments)
|
assert isinstance(arguments, Arguments)
|
||||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
||||||
assert isinstance(arguments.parser, argparse.ArgumentParser)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_defaults() -> None:
|
def test_parse_args_defaults() -> None:
|
||||||
args = Arguments([], '').get_parsed_arg()
|
args = Arguments([], '').get_parsed_arg()
|
||||||
assert args.config == 'config.json'
|
assert args.config == 'config.json'
|
||||||
assert args.dynamic_whitelist is None
|
assert args.dynamic_whitelist is None
|
||||||
assert args.loglevel == logging.INFO
|
assert args.loglevel == 0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_config() -> None:
|
def test_parse_args_config() -> None:
|
||||||
@ -53,10 +36,10 @@ def test_parse_args_db_url() -> None:
|
|||||||
|
|
||||||
def test_parse_args_verbose() -> None:
|
def test_parse_args_verbose() -> None:
|
||||||
args = Arguments(['-v'], '').get_parsed_arg()
|
args = Arguments(['-v'], '').get_parsed_arg()
|
||||||
assert args.loglevel == logging.DEBUG
|
assert args.loglevel == 1
|
||||||
|
|
||||||
args = Arguments(['--verbose'], '').get_parsed_arg()
|
args = Arguments(['--verbose'], '').get_parsed_arg()
|
||||||
assert args.loglevel == logging.DEBUG
|
assert args.loglevel == 1
|
||||||
|
|
||||||
|
|
||||||
def test_scripts_options() -> None:
|
def test_scripts_options() -> None:
|
||||||
@ -149,15 +132,21 @@ def test_parse_args_backtesting_custom() -> None:
|
|||||||
'backtesting',
|
'backtesting',
|
||||||
'--live',
|
'--live',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--refresh-pairs-cached']
|
'--refresh-pairs-cached',
|
||||||
|
'--strategy-list',
|
||||||
|
'DefaultStrategy',
|
||||||
|
'TestStrategy'
|
||||||
|
]
|
||||||
call_args = Arguments(args, '').get_parsed_arg()
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
assert call_args.config == 'test_conf.json'
|
assert call_args.config == 'test_conf.json'
|
||||||
assert call_args.live is True
|
assert call_args.live is True
|
||||||
assert call_args.loglevel == logging.INFO
|
assert call_args.loglevel == 0
|
||||||
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 == '1m'
|
assert call_args.ticker_interval == '1m'
|
||||||
assert call_args.refresh_pairs is True
|
assert call_args.refresh_pairs is True
|
||||||
|
assert type(call_args.strategy_list) is list
|
||||||
|
assert len(call_args.strategy_list) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_hyperopt_custom() -> None:
|
def test_parse_args_hyperopt_custom() -> None:
|
||||||
@ -170,7 +159,7 @@ def test_parse_args_hyperopt_custom() -> None:
|
|||||||
call_args = Arguments(args, '').get_parsed_arg()
|
call_args = Arguments(args, '').get_parsed_arg()
|
||||||
assert call_args.config == 'test_conf.json'
|
assert call_args.config == 'test_conf.json'
|
||||||
assert call_args.epochs == 20
|
assert call_args.epochs == 20
|
||||||
assert call_args.loglevel == logging.INFO
|
assert call_args.loglevel == 0
|
||||||
assert call_args.subparser == 'hyperopt'
|
assert call_args.subparser == 'hyperopt'
|
||||||
assert call_args.spaces == ['buy']
|
assert call_args.spaces == ['buy']
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
@ -1,76 +1,46 @@
|
|||||||
# pragma pylint: disable=protected-access, invalid-name
|
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for configuration.py
|
|
||||||
"""
|
|
||||||
import json
|
import json
|
||||||
from copy import deepcopy
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from jsonschema import ValidationError
|
from jsonschema import validate, ValidationError
|
||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import Configuration
|
|
||||||
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL
|
|
||||||
from freqtrade.tests.conftest import log_has
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.arguments import Arguments
|
||||||
|
from freqtrade.configuration import Configuration, set_loggers
|
||||||
def test_configuration_object() -> None:
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||||
"""
|
from freqtrade.tests.conftest import log_has
|
||||||
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) -> None:
|
def test_load_config_invalid_pair(default_conf) -> None:
|
||||||
"""
|
default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
||||||
Test the configuration validator with an invalid PAIR format
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['exchange']['pair_whitelist'].append('ETH-BTC')
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_missing_attributes(default_conf) -> None:
|
def test_load_config_missing_attributes(default_conf) -> None:
|
||||||
"""
|
default_conf.pop('exchange')
|
||||||
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.*'):
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
def test_load_config_incorrect_stake_amount(default_conf) -> None:
|
||||||
"""
|
default_conf['stake_amount'] = 'fake'
|
||||||
Test the configuration validator with a missing attribute
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['stake_amount'] = 'fake'
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
configuration._validate_config(conf)
|
configuration._validate_config(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_file(default_conf, mocker, caplog) -> None:
|
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(
|
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -84,13 +54,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
default_conf['max_open_trades'] = 0
|
||||||
Test Configuration._load_config_file() method
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
conf['max_open_trades'] = 0
|
|
||||||
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
Configuration(Namespace())._load_config_file('somefile')
|
Configuration(Namespace())._load_config_file('somefile')
|
||||||
@ -99,9 +65,6 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_config_file_exception(mocker) -> None:
|
def test_load_config_file_exception(mocker) -> None:
|
||||||
"""
|
|
||||||
Test Configuration._load_config_file() method
|
|
||||||
"""
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.configuration.open',
|
'freqtrade.configuration.open',
|
||||||
MagicMock(side_effect=FileNotFoundError('File not found'))
|
MagicMock(side_effect=FileNotFoundError('File not found'))
|
||||||
@ -113,9 +76,6 @@ def test_load_config_file_exception(mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_config(default_conf, mocker) -> None:
|
def test_load_config(default_conf, mocker) -> None:
|
||||||
"""
|
|
||||||
Test Configuration.load_config() without any cli params
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -130,13 +90,9 @@ def test_load_config(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_config_with_params(default_conf, mocker) -> None:
|
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(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
@ -144,7 +100,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
'--db-url', 'sqlite:///someurl',
|
'--db-url', 'sqlite:///someurl',
|
||||||
]
|
]
|
||||||
args = Arguments(arglist, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
@ -161,10 +116,10 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
'--strategy-path', '/some/path'
|
'--strategy-path', '/some/path'
|
||||||
]
|
]
|
||||||
args = Arguments(arglist, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
@ -192,16 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
"""
|
default_conf.update({
|
||||||
Test Configuration.load_config() without any cli params
|
|
||||||
"""
|
|
||||||
custom_conf = deepcopy(default_conf)
|
|
||||||
custom_conf.update({
|
|
||||||
'strategy': 'CustomStrategy',
|
'strategy': 'CustomStrategy',
|
||||||
'strategy_path': '/tmp/strategies',
|
'strategy_path': '/tmp/strategies',
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(custom_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
args = Arguments([], '').get_parsed_arg()
|
args = Arguments([], '').get_parsed_arg()
|
||||||
@ -213,13 +164,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_show_info(default_conf, mocker, caplog) -> None:
|
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||||
"""
|
|
||||||
Test Configuration.show_info()
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--dynamic-whitelist', '10',
|
'--dynamic-whitelist', '10',
|
||||||
'--strategy', 'TestStrategy',
|
'--strategy', 'TestStrategy',
|
||||||
@ -236,19 +183,14 @@ def test_show_info(default_conf, mocker, caplog) -> None:
|
|||||||
'(not applicable with Backtesting and Hyperopt)',
|
'(not applicable with Backtesting and Hyperopt)',
|
||||||
caplog.record_tuples
|
caplog.record_tuples
|
||||||
)
|
)
|
||||||
|
|
||||||
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
|
||||||
assert log_has('Dry run is enabled', caplog.record_tuples)
|
assert log_has('Dry run is enabled', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test setup_configuration() function
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
arglist = [
|
arglist = [
|
||||||
'--config', 'config.json',
|
'--config', 'config.json',
|
||||||
'--strategy', 'DefaultStrategy',
|
'--strategy', 'DefaultStrategy',
|
||||||
@ -275,8 +217,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
assert 'live' not in config
|
assert 'live' not in config
|
||||||
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'realistic_simulation' not in config
|
assert 'position_stacking' not in config
|
||||||
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'refresh_pairs' not in config
|
assert 'refresh_pairs' not in config
|
||||||
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
@ -286,9 +228,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test setup_configuration() function
|
|
||||||
"""
|
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
@ -300,7 +239,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
'backtesting',
|
'backtesting',
|
||||||
'--ticker-interval', '1m',
|
'--ticker-interval', '1m',
|
||||||
'--live',
|
'--live',
|
||||||
'--realistic-simulation',
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions',
|
||||||
'--refresh-pairs-cached',
|
'--refresh-pairs-cached',
|
||||||
'--timerange', ':100',
|
'--timerange', ':100',
|
||||||
'--export', '/bar/foo'
|
'--export', '/bar/foo'
|
||||||
@ -330,9 +270,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
assert 'live' in config
|
assert 'live' in config
|
||||||
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'realistic_simulation'in config
|
assert 'position_stacking'in config
|
||||||
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
|
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
|
||||||
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
|
|
||||||
|
assert 'use_max_market_positions' in config
|
||||||
|
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
|
||||||
|
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
|
||||||
|
|
||||||
assert 'refresh_pairs'in config
|
assert 'refresh_pairs'in config
|
||||||
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
|
||||||
@ -349,7 +292,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
Test setup_configuration() function
|
Test setup_configuration() function
|
||||||
"""
|
"""
|
||||||
@ -357,12 +300,62 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
read_data=json.dumps(default_conf)
|
read_data=json.dumps(default_conf)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
arglist = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'backtesting',
|
||||||
|
'--ticker-interval', '1m',
|
||||||
|
'--export', '/bar/foo',
|
||||||
|
'--strategy-list',
|
||||||
|
'DefaultStrategy',
|
||||||
|
'TestStrategy'
|
||||||
|
]
|
||||||
|
|
||||||
|
args = Arguments(arglist, '').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(
|
||||||
|
'Using data folder: {} ...'.format(config['datadir']),
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
assert 'ticker_interval' in config
|
||||||
|
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
|
||||||
|
assert log_has(
|
||||||
|
'Using ticker_interval: 1m ...',
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'strategy_list' in config
|
||||||
|
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
|
||||||
|
|
||||||
|
assert 'position_stacking' not in config
|
||||||
|
|
||||||
|
assert 'use_max_market_positions' not in config
|
||||||
|
|
||||||
|
assert 'timerange' not in config
|
||||||
|
|
||||||
|
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:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
arglist = [
|
arglist = [
|
||||||
'hyperopt',
|
'hyperopt',
|
||||||
'--epochs', '10',
|
'--epochs', '10',
|
||||||
'--spaces', 'all',
|
'--spaces', 'all',
|
||||||
]
|
]
|
||||||
|
|
||||||
args = Arguments(arglist, '').get_parsed_arg()
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
configuration = Configuration(args)
|
configuration = Configuration(args)
|
||||||
@ -379,26 +372,79 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_check_exchange(default_conf) -> None:
|
def test_check_exchange(default_conf) -> None:
|
||||||
"""
|
|
||||||
Test the configuration validator with a missing attribute
|
|
||||||
"""
|
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
configuration = Configuration(Namespace())
|
configuration = Configuration(Namespace())
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test a valid exchange
|
||||||
conf.get('exchange').update({'name': 'BITTREX'})
|
default_conf.get('exchange').update({'name': 'BITTREX'})
|
||||||
assert configuration.check_exchange(conf)
|
assert configuration.check_exchange(default_conf)
|
||||||
|
|
||||||
# Test a valid exchange
|
# Test a valid exchange
|
||||||
conf.get('exchange').update({'name': 'binance'})
|
default_conf.get('exchange').update({'name': 'binance'})
|
||||||
assert configuration.check_exchange(conf)
|
assert configuration.check_exchange(default_conf)
|
||||||
|
|
||||||
# Test a invalid exchange
|
# Test a invalid exchange
|
||||||
conf.get('exchange').update({'name': 'unknown_exchange'})
|
default_conf.get('exchange').update({'name': 'unknown_exchange'})
|
||||||
configuration.config = conf
|
configuration.config = default_conf
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
match=r'.*Exchange "unknown_exchange" not supported.*'
|
match=r'.*Exchange "unknown_exchange" not supported.*'
|
||||||
):
|
):
|
||||||
configuration.check_exchange(conf)
|
configuration.check_exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
|
||||||
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)))
|
||||||
|
# Prevent setting loggers
|
||||||
|
mocker.patch('freqtrade.configuration.set_loggers', MagicMock)
|
||||||
|
arglist = ['-vvv']
|
||||||
|
args = Arguments(arglist, '').get_parsed_arg()
|
||||||
|
|
||||||
|
configuration = Configuration(args)
|
||||||
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
|
assert validated_conf.get('verbosity') == 3
|
||||||
|
assert log_has('Verbosity set to 3', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_loggers() -> None:
|
||||||
|
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
|
||||||
|
logging.getLogger('requests').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger("urllib3").setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
|
||||||
|
logging.getLogger('telegram').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
previous_value1 = logging.getLogger('requests').level
|
||||||
|
previous_value2 = logging.getLogger('ccxt.base.exchange').level
|
||||||
|
previous_value3 = logging.getLogger('telegram').level
|
||||||
|
|
||||||
|
set_loggers()
|
||||||
|
|
||||||
|
value1 = logging.getLogger('requests').level
|
||||||
|
assert previous_value1 is not value1
|
||||||
|
assert value1 is logging.INFO
|
||||||
|
|
||||||
|
value2 = logging.getLogger('ccxt.base.exchange').level
|
||||||
|
assert previous_value2 is not value2
|
||||||
|
assert value2 is logging.INFO
|
||||||
|
|
||||||
|
value3 = logging.getLogger('telegram').level
|
||||||
|
assert previous_value3 is not value3
|
||||||
|
assert value3 is logging.INFO
|
||||||
|
|
||||||
|
set_loggers(log_level=2)
|
||||||
|
|
||||||
|
assert logging.getLogger('requests').level is logging.DEBUG
|
||||||
|
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
|
||||||
|
assert logging.getLogger('telegram').level is logging.INFO
|
||||||
|
|
||||||
|
set_loggers(log_level=3)
|
||||||
|
|
||||||
|
assert logging.getLogger('requests').level is logging.DEBUG
|
||||||
|
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
|
||||||
|
assert logging.getLogger('telegram').level is logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_default_conf(default_conf) -> None:
|
||||||
|
validate(default_conf, constants.CONF_SCHEMA)
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
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)
|
|
@ -2,33 +2,31 @@
|
|||||||
|
|
||||||
import pandas
|
import pandas
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade.optimize import load_data
|
from freqtrade.optimize import load_data
|
||||||
from freqtrade.strategy.resolver import StrategyResolver
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
_pairs = ['ETH/BTC']
|
_pairs = ['ETH/BTC']
|
||||||
|
|
||||||
|
|
||||||
def load_dataframe_pair(pairs):
|
def load_dataframe_pair(pairs, strategy):
|
||||||
ld = load_data(None, ticker_interval='5m', pairs=pairs)
|
ld = load_data(None, ticker_interval='5m', pairs=pairs)
|
||||||
assert isinstance(ld, dict)
|
assert isinstance(ld, dict)
|
||||||
assert isinstance(pairs[0], str)
|
assert isinstance(pairs[0], str)
|
||||||
dataframe = ld[pairs[0]]
|
dataframe = ld[pairs[0]]
|
||||||
|
|
||||||
analyze = Analyze({'strategy': 'DefaultStrategy'})
|
dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]})
|
||||||
dataframe = analyze.analyze_ticker(dataframe)
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_load():
|
def test_dataframe_load():
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs, strategy)
|
||||||
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'})
|
strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
|
||||||
dataframe = load_dataframe_pair(_pairs)
|
dataframe = load_dataframe_pair(_pairs, strategy)
|
||||||
assert 'high' in dataframe.columns
|
assert 'high' in dataframe.columns
|
||||||
assert 'low' in dataframe.columns
|
assert 'low' in dataframe.columns
|
||||||
assert 'close' in dataframe.columns
|
assert 'close' in dataframe.columns
|
||||||
|
@ -5,7 +5,6 @@ import time
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
|
||||||
@ -184,6 +183,24 @@ def test_fiat_convert_without_network(mocker):
|
|||||||
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
CryptoToFiatConverter._coinmarketcap = cmc_temp
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_invalid_response(mocker, caplog):
|
||||||
|
# Because CryptoToFiatConverter is a Singleton we reset the listings
|
||||||
|
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.fiat_convert.Market',
|
||||||
|
listings=listmock,
|
||||||
|
)
|
||||||
|
# with pytest.raises(RequestEsxception):
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
fiat_convert._cryptomap = {}
|
||||||
|
fiat_convert._load_cryptomap()
|
||||||
|
|
||||||
|
length_cryptomap = len(fiat_convert._cryptomap)
|
||||||
|
assert length_cryptomap == 0
|
||||||
|
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_convert_amount(mocker):
|
def test_convert_amount(mocker):
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,8 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.indicator_helpers import went_up, went_down
|
from freqtrade.indicator_helpers import went_down, went_up
|
||||||
|
|
||||||
|
|
||||||
def test_went_up():
|
def test_went_up():
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
"""
|
# pragma pylint: disable=missing-docstring
|
||||||
Unit test file for main.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -11,7 +8,7 @@ import pytest
|
|||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.main import main, set_loggers, reconfigure
|
from freqtrade.main import main, reconfigure
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.conftest import log_has, patch_exchange
|
from freqtrade.tests.conftest import log_has, patch_exchange
|
||||||
|
|
||||||
@ -27,49 +24,24 @@ def test_parse_args_backtesting(mocker) -> None:
|
|||||||
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'
|
||||||
assert call_args.live is False
|
assert call_args.live is False
|
||||||
assert call_args.loglevel == 20
|
assert call_args.loglevel == 0
|
||||||
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 is None
|
assert call_args.ticker_interval is None
|
||||||
|
|
||||||
|
|
||||||
def test_main_start_hyperopt(mocker) -> None:
|
def test_main_start_hyperopt(mocker) -> None:
|
||||||
"""
|
|
||||||
Test that main() can start hyperopt
|
|
||||||
"""
|
|
||||||
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
||||||
main(['hyperopt'])
|
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'
|
||||||
assert call_args.loglevel == 20
|
assert call_args.loglevel == 0
|
||||||
assert call_args.subparser == 'hyperopt'
|
assert call_args.subparser == 'hyperopt'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
|
|
||||||
|
|
||||||
def test_set_loggers() -> None:
|
|
||||||
"""
|
|
||||||
Test set_loggers() update the logger level for third-party libraries
|
|
||||||
"""
|
|
||||||
previous_value1 = logging.getLogger('requests.packages.urllib3').level
|
|
||||||
previous_value2 = logging.getLogger('telegram').level
|
|
||||||
|
|
||||||
set_loggers()
|
|
||||||
|
|
||||||
value1 = logging.getLogger('requests.packages.urllib3').level
|
|
||||||
assert previous_value1 is not value1
|
|
||||||
assert value1 is logging.INFO
|
|
||||||
|
|
||||||
value2 = logging.getLogger('telegram').level
|
|
||||||
assert previous_value2 is not value2
|
|
||||||
assert value2 is logging.INFO
|
|
||||||
|
|
||||||
|
|
||||||
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test main() function
|
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
@ -81,7 +53,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
args = ['-c', 'config.json.example']
|
args = ['-c', 'config.json.example']
|
||||||
@ -94,10 +65,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test main() function
|
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
@ -109,7 +76,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
args = ['-c', 'config.json.example']
|
args = ['-c', 'config.json.example']
|
||||||
@ -122,10 +88,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test main() function
|
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
@ -137,7 +99,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
args = ['-c', 'config.json.example']
|
args = ['-c', 'config.json.example']
|
||||||
@ -150,10 +111,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
||||||
"""
|
|
||||||
Test main() function
|
|
||||||
In this test we are skipping the while True loop by throwing an exception.
|
|
||||||
"""
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
@ -165,7 +122,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
|||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
# Raise exception as side effect to avoid endless loop
|
# Raise exception as side effect to avoid endless loop
|
||||||
@ -181,7 +137,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_reconfigure(mocker, default_conf) -> None:
|
def test_reconfigure(mocker, default_conf) -> None:
|
||||||
""" Test recreate() function """
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
@ -193,7 +148,6 @@ def test_reconfigure(mocker, default_conf) -> None:
|
|||||||
'freqtrade.configuration.Configuration._load_config_file',
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
lambda *args, **kwargs: default_conf
|
lambda *args, **kwargs: default_conf
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
|
|
||||||
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
@ -1,34 +1,23 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
|
||||||
"""
|
|
||||||
Unit test file for misc.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
|
||||||
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
|
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
|
||||||
common_datearray, file_dump_json, format_ms_time)
|
file_dump_json, format_ms_time, shorten_date)
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
def test_shorten_date() -> None:
|
def test_shorten_date() -> None:
|
||||||
"""
|
|
||||||
Test shorten_date() function
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
|
||||||
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
|
||||||
assert shorten_date(str_data) == str_shorten_data
|
assert shorten_date(str_data) == str_shorten_data
|
||||||
|
|
||||||
|
|
||||||
def test_datesarray_to_datetimearray(ticker_history):
|
def test_datesarray_to_datetimearray(ticker_history):
|
||||||
"""
|
dataframes = parse_ticker_dataframe(ticker_history)
|
||||||
Test datesarray_to_datetimearray() function
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
|
|
||||||
dates = datesarray_to_datetimearray(dataframes['date'])
|
dates = datesarray_to_datetimearray(dataframes['date'])
|
||||||
|
|
||||||
assert isinstance(dates[0], datetime.datetime)
|
assert isinstance(dates[0], datetime.datetime)
|
||||||
@ -43,14 +32,10 @@ def test_datesarray_to_datetimearray(ticker_history):
|
|||||||
|
|
||||||
|
|
||||||
def test_common_datearray(default_conf) -> None:
|
def test_common_datearray(default_conf) -> None:
|
||||||
"""
|
strategy = DefaultStrategy(default_conf)
|
||||||
Test common_datearray()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
analyze = Analyze(default_conf)
|
|
||||||
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
|
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
|
|
||||||
dates = common_datearray(dataframes)
|
dates = common_datearray(dataframes)
|
||||||
|
|
||||||
@ -60,10 +45,6 @@ def test_common_datearray(default_conf) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_file_dump_json(mocker) -> None:
|
def test_file_dump_json(mocker) -> None:
|
||||||
"""
|
|
||||||
Test file_dump_json()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||||
json_dump = mocker.patch('json.dump', MagicMock())
|
json_dump = mocker.patch('json.dump', MagicMock())
|
||||||
file_dump_json('somefile', [1, 2, 3])
|
file_dump_json('somefile', [1, 2, 3])
|
||||||
@ -77,10 +58,6 @@ def test_file_dump_json(mocker) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_format_ms_time() -> None:
|
def test_format_ms_time() -> None:
|
||||||
"""
|
|
||||||
test format_ms_time()
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
# Date 2018-04-10 18:02:01
|
# Date 2018-04-10 18:02:01
|
||||||
date_in_epoch_ms = 1523383321000
|
date_in_epoch_ms = 1523383321000
|
||||||
date = format_ms_time(date_in_epoch_ms)
|
date = format_ms_time(date_in_epoch_ms)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
from copy import deepcopy
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade import constants, OperationalException
|
from freqtrade import OperationalException, constants
|
||||||
from freqtrade.persistence import Trade, init, clean_dry_run_db
|
from freqtrade.persistence import Trade, clean_dry_run_db, init
|
||||||
|
from freqtrade.tests.conftest import log_has
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
@ -22,46 +22,40 @@ def test_init_create_session(default_conf):
|
|||||||
|
|
||||||
|
|
||||||
def test_init_custom_db_url(default_conf, mocker):
|
def test_init_custom_db_url(default_conf, mocker):
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
|
|
||||||
# Update path to a value other than default, but still in-memory
|
# Update path to a value other than default, but still in-memory
|
||||||
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(conf)
|
init(default_conf)
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
|
||||||
|
|
||||||
|
|
||||||
def test_init_invalid_db_url(default_conf):
|
def test_init_invalid_db_url(default_conf):
|
||||||
conf = deepcopy(default_conf)
|
|
||||||
|
|
||||||
# Update path to a value other than default, but still in-memory
|
# Update path to a value other than default, but still in-memory
|
||||||
conf.update({'db_url': 'unknown:///some.url'})
|
default_conf.update({'db_url': 'unknown:///some.url'})
|
||||||
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
|
||||||
init(conf)
|
init(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_init_prod_db(default_conf, mocker):
|
def test_init_prod_db(default_conf, mocker):
|
||||||
conf = deepcopy(default_conf)
|
default_conf.update({'dry_run': False})
|
||||||
conf.update({'dry_run': False})
|
default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||||
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
|
||||||
|
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(conf)
|
init(default_conf)
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
|
|
||||||
def test_init_dryrun_db(default_conf, mocker):
|
def test_init_dryrun_db(default_conf, mocker):
|
||||||
conf = deepcopy(default_conf)
|
default_conf.update({'dry_run': True})
|
||||||
conf.update({'dry_run': True})
|
default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
||||||
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
|
|
||||||
|
|
||||||
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
|
||||||
|
|
||||||
init(conf)
|
init(default_conf)
|
||||||
assert create_engine_mock.call_count == 1
|
assert create_engine_mock.call_count == 1
|
||||||
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
|
||||||
|
|
||||||
@ -400,13 +394,17 @@ def test_migrate_old(mocker, default_conf, fee):
|
|||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
assert trade.pair == "ETC/BTC"
|
assert trade.pair == "ETC/BTC"
|
||||||
assert trade.exchange == "bittrex"
|
assert trade.exchange == "bittrex"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_new(mocker, default_conf, fee):
|
def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
amount = 103.223
|
amount = 103.223
|
||||||
|
# Always create all columns apart from the last!
|
||||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
id INTEGER NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
exchange VARCHAR NOT NULL,
|
exchange VARCHAR NOT NULL,
|
||||||
@ -421,14 +419,21 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
open_date DATETIME NOT NULL,
|
open_date DATETIME NOT NULL,
|
||||||
close_date DATETIME,
|
close_date DATETIME,
|
||||||
open_order_id VARCHAR,
|
open_order_id VARCHAR,
|
||||||
|
stop_loss FLOAT,
|
||||||
|
initial_stop_loss FLOAT,
|
||||||
|
max_rate FLOAT,
|
||||||
|
sell_reason VARCHAR,
|
||||||
|
strategy VARCHAR,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
CHECK (is_open IN (0, 1))
|
CHECK (is_open IN (0, 1))
|
||||||
);"""
|
);"""
|
||||||
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
|
||||||
open_rate, stake_amount, amount, open_date)
|
open_rate, stake_amount, amount, open_date,
|
||||||
|
stop_loss, initial_stop_loss, max_rate)
|
||||||
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
VALUES ('binance', 'ETC/BTC', 1, {fee},
|
||||||
0.00258580, {stake}, {amount},
|
0.00258580, {stake}, {amount},
|
||||||
'2019-11-28 12:44:24.000000')
|
'2019-11-28 12:44:24.000000',
|
||||||
|
0.0, 0.0, 0.0)
|
||||||
""".format(fee=fee.return_value,
|
""".format(fee=fee.return_value,
|
||||||
stake=default_conf.get("stake_amount"),
|
stake=default_conf.get("stake_amount"),
|
||||||
amount=amount
|
amount=amount
|
||||||
@ -439,6 +444,11 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
# Create table using the old format
|
# Create table using the old format
|
||||||
engine.execute(create_table_old)
|
engine.execute(create_table_old)
|
||||||
engine.execute(insert_table_old)
|
engine.execute(insert_table_old)
|
||||||
|
|
||||||
|
# fake previous backup
|
||||||
|
engine.execute("create table trades_bak as select * from trades")
|
||||||
|
|
||||||
|
engine.execute("create table trades_bak1 as select * from trades")
|
||||||
# Run init to test migration
|
# Run init to test migration
|
||||||
init(default_conf)
|
init(default_conf)
|
||||||
|
|
||||||
@ -453,3 +463,116 @@ def test_migrate_new(mocker, default_conf, fee):
|
|||||||
assert trade.stake_amount == default_conf.get("stake_amount")
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
assert trade.pair == "ETC/BTC"
|
assert trade.pair == "ETC/BTC"
|
||||||
assert trade.exchange == "binance"
|
assert trade.exchange == "binance"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
assert trade.sell_reason is None
|
||||||
|
assert trade.strategy is None
|
||||||
|
assert trade.ticker_interval is None
|
||||||
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||||
|
"""
|
||||||
|
Test Database migration (starting with new pairformat)
|
||||||
|
"""
|
||||||
|
amount = 103.223
|
||||||
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
id INTEGER NOT NULL,
|
||||||
|
exchange VARCHAR NOT NULL,
|
||||||
|
pair VARCHAR NOT NULL,
|
||||||
|
is_open BOOLEAN NOT NULL,
|
||||||
|
fee_open FLOAT NOT NULL,
|
||||||
|
fee_close FLOAT NOT NULL,
|
||||||
|
open_rate FLOAT,
|
||||||
|
close_rate FLOAT,
|
||||||
|
close_profit FLOAT,
|
||||||
|
stake_amount FLOAT NOT NULL,
|
||||||
|
amount FLOAT,
|
||||||
|
open_date DATETIME NOT NULL,
|
||||||
|
close_date DATETIME,
|
||||||
|
open_order_id VARCHAR,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CHECK (is_open IN (0, 1))
|
||||||
|
);"""
|
||||||
|
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
|
||||||
|
open_rate, stake_amount, amount, open_date)
|
||||||
|
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
|
||||||
|
0.00258580, {stake}, {amount},
|
||||||
|
'2019-11-28 12:44:24.000000')
|
||||||
|
""".format(fee=fee.return_value,
|
||||||
|
stake=default_conf.get("stake_amount"),
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
engine = create_engine('sqlite://')
|
||||||
|
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
|
||||||
|
|
||||||
|
# Create table using the old format
|
||||||
|
engine.execute(create_table_old)
|
||||||
|
engine.execute(insert_table_old)
|
||||||
|
|
||||||
|
# Run init to test migration
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
|
||||||
|
trade = Trade.query.filter(Trade.id == 1).first()
|
||||||
|
assert trade.fee_open == fee.return_value
|
||||||
|
assert trade.fee_close == fee.return_value
|
||||||
|
assert trade.open_rate_requested is None
|
||||||
|
assert trade.close_rate_requested is None
|
||||||
|
assert trade.is_open == 1
|
||||||
|
assert trade.amount == amount
|
||||||
|
assert trade.stake_amount == default_conf.get("stake_amount")
|
||||||
|
assert trade.pair == "ETC/BTC"
|
||||||
|
assert trade.exchange == "binance"
|
||||||
|
assert trade.max_rate == 0.0
|
||||||
|
assert trade.stop_loss == 0.0
|
||||||
|
assert trade.initial_stop_loss == 0.0
|
||||||
|
assert log_has("trying trades_bak0", caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
|
||||||
|
assert trade.stop_loss == 0.95
|
||||||
|
assert trade.max_rate == 1
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Get percent of profit with a lowre rate
|
||||||
|
trade.adjust_stop_loss(0.96, 0.05)
|
||||||
|
assert trade.stop_loss == 0.95
|
||||||
|
assert trade.max_rate == 1
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
|
trade.adjust_stop_loss(1.3, -0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.17
|
||||||
|
assert trade.max_rate == 1.3
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# current rate lower again ... should not change
|
||||||
|
trade.adjust_stop_loss(1.2, 0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.17
|
||||||
|
assert trade.max_rate == 1.3
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# current rate higher... should raise stoploss
|
||||||
|
trade.adjust_stop_loss(1.4, 0.1)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.26
|
||||||
|
assert trade.max_rate == 1.4
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
|
||||||
|
# Initial is true but stop_loss set - so doesn't do anything
|
||||||
|
trade.adjust_stop_loss(1.7, 0.1, True)
|
||||||
|
assert round(trade.stop_loss, 8) == 1.26
|
||||||
|
assert trade.max_rate == 1.4
|
||||||
|
assert trade.initial_stop_loss == 0.95
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
"""
|
|
||||||
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')
|
|
16
freqtrade/tests/test_talib.py
Normal file
16
freqtrade/tests/test_talib.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def test_talib_bollingerbands_near_zero_values():
|
||||||
|
inputs = pd.DataFrame([
|
||||||
|
{'close': 0.00000010},
|
||||||
|
{'close': 0.00000011},
|
||||||
|
{'close': 0.00000012},
|
||||||
|
{'close': 0.00000013},
|
||||||
|
{'close': 0.00000014}
|
||||||
|
])
|
||||||
|
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
|
||||||
|
assert (bollinger['upperband'][3] != bollinger['middleband'][3])
|
@ -1,6 +1,6 @@
|
|||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib && ./configure && make && sudo make install && cd ..
|
cd ta-lib && sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
|
||||||
else
|
else
|
||||||
echo "TA-lib already installed, skipping download and build."
|
echo "TA-lib already installed, skipping download and build."
|
||||||
cd ta-lib && sudo make install && cd ..
|
cd ta-lib && sudo make install && cd ..
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
ccxt==1.14.256
|
ccxt==1.17.126
|
||||||
SQLAlchemy==1.2.8
|
SQLAlchemy==1.2.10
|
||||||
python-telegram-bot==10.1.0
|
python-telegram-bot==10.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
cachetools==2.1.0
|
cachetools==2.1.0
|
||||||
requests==2.19.1
|
requests==2.19.1
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.23.1
|
pandas==0.23.4
|
||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.2
|
||||||
scipy==1.1.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.14.5
|
numpy==1.15.0
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
pytest==3.6.2
|
pytest==3.7.1
|
||||||
pytest-mock==1.10.0
|
pytest-mock==1.10.0
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
hyperopt==0.1
|
|
||||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
|
||||||
networkx==1.11 # pyup: ignore
|
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
|
# Required for hyperopt
|
||||||
|
scikit-optimize==0.5.2
|
||||||
|
|
||||||
# Required for plotting data
|
# Required for plotting data
|
||||||
#plotly==2.7.0
|
#plotly==3.1.1
|
||||||
|
@ -143,15 +143,14 @@ def convert_main(args: Namespace) -> None:
|
|||||||
interval = str_interval
|
interval = str_interval
|
||||||
break
|
break
|
||||||
# change order on pairs if old ticker interval found
|
# change order on pairs if old ticker interval found
|
||||||
|
|
||||||
filename_new = path.join(path.dirname(filename),
|
filename_new = path.join(path.dirname(filename),
|
||||||
"{}_{}-{}.json".format(currencies[1],
|
f"{currencies[1]}_{currencies[0]}-{interval}.json")
|
||||||
currencies[0], interval))
|
|
||||||
|
|
||||||
elif ret_string:
|
elif ret_string:
|
||||||
interval = ret_string.group(0)
|
interval = ret_string.group(0)
|
||||||
filename_new = path.join(path.dirname(filename),
|
filename_new = path.join(path.dirname(filename),
|
||||||
"{}_{}-{}.json".format(currencies[0],
|
f"{currencies[0]}_{currencies[1]}-{interval}.json")
|
||||||
currencies[1], interval))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning("file %s could not be converted, interval not found", filename)
|
logger.warning("file %s could not be converted, interval not found", filename)
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
"""This script generate json data from bittrex"""
|
"""This script generate json data from bittrex"""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import os
|
from pathlib import Path
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from freqtrade import (arguments, misc)
|
from freqtrade import arguments
|
||||||
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
|
from freqtrade.optimize import download_backtesting_testdata
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_DL_PATH = 'user_data/data'
|
DEFAULT_DL_PATH = 'user_data/data'
|
||||||
|
|
||||||
@ -17,25 +20,27 @@ args = arguments.parse_args()
|
|||||||
|
|
||||||
timeframes = args.timeframes
|
timeframes = args.timeframes
|
||||||
|
|
||||||
dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange)
|
dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange)
|
||||||
if args.export:
|
if args.export:
|
||||||
dl_path = args.export
|
dl_path = Path(args.export)
|
||||||
|
|
||||||
if not os.path.isdir(dl_path):
|
if not dl_path.is_dir():
|
||||||
sys.exit(f'Directory {dl_path} does not exist.')
|
sys.exit(f'Directory {dl_path} does not exist.')
|
||||||
|
|
||||||
pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json')
|
pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
|
||||||
if not os.path.isfile(pairs_file):
|
if not pairs_file.exists():
|
||||||
sys.exit(f'No pairs file found with path {pairs_file}.')
|
sys.exit(f'No pairs file found with path {pairs_file}.')
|
||||||
|
|
||||||
with open(pairs_file) as file:
|
with pairs_file.open() as file:
|
||||||
PAIRS = list(set(json.load(file)))
|
PAIRS = list(set(json.load(file)))
|
||||||
|
|
||||||
PAIRS.sort()
|
PAIRS.sort()
|
||||||
|
|
||||||
since_time = None
|
|
||||||
|
timerange = TimeRange()
|
||||||
if args.days:
|
if args.days:
|
||||||
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
|
time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
|
||||||
|
timerange = arguments.parse_timerange(f'{time_since}-')
|
||||||
|
|
||||||
|
|
||||||
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
print(f'About to download pairs: {PAIRS} to {dl_path}')
|
||||||
@ -59,21 +64,18 @@ for pair in PAIRS:
|
|||||||
print(f"skipping pair {pair}")
|
print(f"skipping pair {pair}")
|
||||||
continue
|
continue
|
||||||
for tick_interval in timeframes:
|
for tick_interval in timeframes:
|
||||||
print(f'downloading pair {pair}, interval {tick_interval}')
|
|
||||||
|
|
||||||
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
|
|
||||||
if not data:
|
|
||||||
print('\tNo data was downloaded')
|
|
||||||
break
|
|
||||||
|
|
||||||
print('\tData was downloaded for period %s - %s' % (
|
|
||||||
arrow.get(data[0][0] / 1000).format(),
|
|
||||||
arrow.get(data[-1][0] / 1000).format()))
|
|
||||||
|
|
||||||
# save data
|
|
||||||
pair_print = pair.replace('/', '_')
|
pair_print = pair.replace('/', '_')
|
||||||
filename = f'{pair_print}-{tick_interval}.json'
|
filename = f'{pair_print}-{tick_interval}.json'
|
||||||
misc.file_dump_json(os.path.join(dl_path, filename), data)
|
dl_file = dl_path.joinpath(filename)
|
||||||
|
if args.erase and dl_file.exists():
|
||||||
|
print(f'Deleting existing data for pair {pair}, interval {tick_interval}')
|
||||||
|
dl_file.unlink()
|
||||||
|
|
||||||
|
print(f'downloading pair {pair}, interval {tick_interval}')
|
||||||
|
download_backtesting_testdata(str(dl_path), exchange=exchange,
|
||||||
|
pair=pair,
|
||||||
|
tick_interval=tick_interval,
|
||||||
|
timerange=timerange)
|
||||||
|
|
||||||
|
|
||||||
if pairs_not_available:
|
if pairs_not_available:
|
||||||
|
93
scripts/get_market_pairs.py
Normal file
93
scripts/get_market_pairs.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
sys.path.append(root + '/python')
|
||||||
|
|
||||||
|
import ccxt # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def style(s, style):
|
||||||
|
return style + s + '\033[0m'
|
||||||
|
|
||||||
|
|
||||||
|
def green(s):
|
||||||
|
return style(s, '\033[92m')
|
||||||
|
|
||||||
|
|
||||||
|
def blue(s):
|
||||||
|
return style(s, '\033[94m')
|
||||||
|
|
||||||
|
|
||||||
|
def yellow(s):
|
||||||
|
return style(s, '\033[93m')
|
||||||
|
|
||||||
|
|
||||||
|
def red(s):
|
||||||
|
return style(s, '\033[91m')
|
||||||
|
|
||||||
|
|
||||||
|
def pink(s):
|
||||||
|
return style(s, '\033[95m')
|
||||||
|
|
||||||
|
|
||||||
|
def bold(s):
|
||||||
|
return style(s, '\033[1m')
|
||||||
|
|
||||||
|
|
||||||
|
def underline(s):
|
||||||
|
return style(s, '\033[4m')
|
||||||
|
|
||||||
|
|
||||||
|
def dump(*args):
|
||||||
|
print(' '.join([str(arg) for arg in args]))
|
||||||
|
|
||||||
|
|
||||||
|
def print_supported_exchanges():
|
||||||
|
dump('Supported exchanges:', green(', '.join(ccxt.exchanges)))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
id = sys.argv[1] # get exchange id from command line arguments
|
||||||
|
|
||||||
|
|
||||||
|
# check if the exchange is supported by ccxt
|
||||||
|
exchange_found = id in ccxt.exchanges
|
||||||
|
|
||||||
|
if exchange_found:
|
||||||
|
dump('Instantiating', green(id), 'exchange')
|
||||||
|
|
||||||
|
# instantiate the exchange by id
|
||||||
|
exchange = getattr(ccxt, id)({
|
||||||
|
# 'proxy':'https://cors-anywhere.herokuapp.com/',
|
||||||
|
})
|
||||||
|
|
||||||
|
# load all markets from the exchange
|
||||||
|
markets = exchange.load_markets()
|
||||||
|
|
||||||
|
# output a list of all market symbols
|
||||||
|
dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols)
|
||||||
|
|
||||||
|
tuples = list(ccxt.Exchange.keysort(markets).items())
|
||||||
|
|
||||||
|
# debug
|
||||||
|
for (k, v) in tuples:
|
||||||
|
print(v)
|
||||||
|
|
||||||
|
# output a table of all markets
|
||||||
|
dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote')))
|
||||||
|
|
||||||
|
for (k, v) in tuples:
|
||||||
|
dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote']))
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
dump('Exchange ' + red(id) + ' not found')
|
||||||
|
print_supported_exchanges()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
dump('[' + type(e).__name__ + ']', str(e))
|
||||||
|
dump("Usage: python " + sys.argv[0], green('id'))
|
||||||
|
print_supported_exchanges()
|
||||||
|
|
@ -24,27 +24,76 @@ Example of usage:
|
|||||||
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
|
||||||
--indicators2 fastk,fastd
|
--indicators2 fastk,fastd
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from pathlib import Path
|
||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
import plotly.graph_objs as go
|
import plotly.graph_objs as go
|
||||||
|
import pytz
|
||||||
|
|
||||||
from plotly import tools
|
from plotly import tools
|
||||||
from plotly.offline import plot
|
from plotly.offline import plot
|
||||||
|
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
from freqtrade import persistence
|
from freqtrade import persistence
|
||||||
from freqtrade.analyze import Analyze
|
from freqtrade.arguments import Arguments, TimeRange
|
||||||
from freqtrade.arguments import Arguments
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.optimize.backtesting import setup_configuration
|
from freqtrade.optimize.backtesting import setup_configuration
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_CONF: Dict[str, Any] = {}
|
_CONF: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
timeZone = pytz.UTC
|
||||||
|
|
||||||
|
|
||||||
|
def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame:
|
||||||
|
trades: pd.DataFrame = pd.DataFrame()
|
||||||
|
if args.db_url:
|
||||||
|
persistence.init(_CONF)
|
||||||
|
columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"]
|
||||||
|
|
||||||
|
for x in Trade.query.all():
|
||||||
|
print("date: {}".format(x.open_date))
|
||||||
|
|
||||||
|
trades = pd.DataFrame([(t.pair, t.calc_profit(),
|
||||||
|
t.open_date.replace(tzinfo=timeZone),
|
||||||
|
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
|
||||||
|
t.open_rate, t.close_rate,
|
||||||
|
t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None)
|
||||||
|
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
|
||||||
|
columns=columns)
|
||||||
|
|
||||||
|
elif args.exportfilename:
|
||||||
|
file = Path(args.exportfilename)
|
||||||
|
# must align with columns in backtest.py
|
||||||
|
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||||
|
"open_rate", "close_rate", "open_at_end"]
|
||||||
|
with file.open() as f:
|
||||||
|
data = json.load(f)
|
||||||
|
trades = pd.DataFrame(data, columns=columns)
|
||||||
|
trades = trades.loc[trades["pair"] == pair]
|
||||||
|
if timerange:
|
||||||
|
if timerange.starttype == 'date':
|
||||||
|
trades = trades.loc[trades["opents"] >= timerange.startts]
|
||||||
|
if timerange.stoptype == 'date':
|
||||||
|
trades = trades.loc[trades["opents"] <= timerange.stopts]
|
||||||
|
|
||||||
|
trades['opents'] = pd.to_datetime(trades['opents'],
|
||||||
|
unit='s',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
trades['closets'] = pd.to_datetime(trades['closets'],
|
||||||
|
unit='s',
|
||||||
|
utc=True,
|
||||||
|
infer_datetime_format=True)
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
def plot_analyzed_dataframe(args: Namespace) -> None:
|
def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
@ -56,6 +105,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
# Load the configuration
|
# Load the configuration
|
||||||
_CONF.update(setup_configuration(args))
|
_CONF.update(setup_configuration(args))
|
||||||
|
|
||||||
|
print(_CONF)
|
||||||
# Set the pair to audit
|
# Set the pair to audit
|
||||||
pair = args.pair
|
pair = args.pair
|
||||||
|
|
||||||
@ -72,7 +122,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
|
|
||||||
# Load the strategy
|
# Load the strategy
|
||||||
try:
|
try:
|
||||||
analyze = Analyze(_CONF)
|
strategy = StrategyResolver(_CONF).strategy
|
||||||
exchange = Exchange(_CONF)
|
exchange = Exchange(_CONF)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
@ -82,20 +132,21 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
exit()
|
exit()
|
||||||
|
|
||||||
# Set the ticker to use
|
# Set the ticker to use
|
||||||
tick_interval = analyze.get_ticker_interval()
|
tick_interval = strategy.ticker_interval
|
||||||
|
|
||||||
# Load pair tickers
|
# Load pair tickers
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pair.')
|
||||||
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
|
tickers[pair] = exchange.get_candle_history(pair, tick_interval)
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(
|
tickers = optimize.load_data(
|
||||||
datadir=_CONF.get("datadir"),
|
datadir=_CONF.get("datadir"),
|
||||||
pairs=[pair],
|
pairs=[pair],
|
||||||
ticker_interval=tick_interval,
|
ticker_interval=tick_interval,
|
||||||
refresh_pairs=_CONF.get('refresh_pairs', False),
|
refresh_pairs=_CONF.get('refresh_pairs', False),
|
||||||
timerange=timerange
|
timerange=timerange,
|
||||||
|
exchange=Exchange(_CONF)
|
||||||
)
|
)
|
||||||
|
|
||||||
# No ticker found, or impossible to download
|
# No ticker found, or impossible to download
|
||||||
@ -103,30 +154,31 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
|||||||
exit()
|
exit()
|
||||||
|
|
||||||
# Get trades already made from the DB
|
# Get trades already made from the DB
|
||||||
trades: List[Trade] = []
|
trades = load_trades(args, pair, timerange)
|
||||||
if args.db_url:
|
|
||||||
persistence.init(_CONF)
|
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||||
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
|
|
||||||
|
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
|
||||||
dataframe = dataframes[pair]
|
dataframe = dataframes[pair]
|
||||||
dataframe = analyze.populate_buy_trend(dataframe)
|
dataframe = strategy.advise_buy(dataframe, {'pair': pair})
|
||||||
dataframe = analyze.populate_sell_trend(dataframe)
|
dataframe = strategy.advise_sell(dataframe, {'pair': pair})
|
||||||
|
|
||||||
if len(dataframe.index) > 750:
|
if len(dataframe.index) > args.plot_limit:
|
||||||
logger.warning('Ticker contained more than 750 candles, clipping.')
|
logger.warning('Ticker contained more than %s candles as defined '
|
||||||
|
'with --plot-limit, clipping.', args.plot_limit)
|
||||||
|
dataframe = dataframe.tail(args.plot_limit)
|
||||||
|
|
||||||
|
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
|
||||||
fig = generate_graph(
|
fig = generate_graph(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
trades=trades,
|
trades=trades,
|
||||||
data=dataframe.tail(750),
|
data=dataframe,
|
||||||
args=args
|
args=args
|
||||||
)
|
)
|
||||||
|
|
||||||
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html'))
|
plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
|
||||||
|
|
||||||
|
|
||||||
def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots:
|
||||||
"""
|
"""
|
||||||
Generate the graph from the data generated by Backtesting or from DB
|
Generate the graph from the data generated by Backtesting or from DB
|
||||||
:param pair: Pair to Display on the graph
|
:param pair: Pair to Display on the graph
|
||||||
@ -187,8 +239,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
)
|
)
|
||||||
|
|
||||||
trade_buys = go.Scattergl(
|
trade_buys = go.Scattergl(
|
||||||
x=[t.open_date.isoformat() for t in trades],
|
x=trades["opents"],
|
||||||
y=[t.open_rate for t in trades],
|
y=trades["open_rate"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='trade_buy',
|
name='trade_buy',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
@ -199,8 +251,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
trade_sells = go.Scattergl(
|
trade_sells = go.Scattergl(
|
||||||
x=[t.close_date.isoformat() for t in trades],
|
x=trades["closets"],
|
||||||
y=[t.close_rate for t in trades],
|
y=trades["close_rate"],
|
||||||
mode='markers',
|
mode='markers',
|
||||||
name='trade_sell',
|
name='trade_sell',
|
||||||
marker=dict(
|
marker=dict(
|
||||||
@ -219,7 +271,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
x=data.date,
|
x=data.date,
|
||||||
y=data.bb_lowerband,
|
y=data.bb_lowerband,
|
||||||
name='BB lower',
|
name='BB lower',
|
||||||
line={'color': "transparent"},
|
line={'color': 'rgba(255,255,255,0)'},
|
||||||
)
|
)
|
||||||
bb_upper = go.Scatter(
|
bb_upper = go.Scatter(
|
||||||
x=data.date,
|
x=data.date,
|
||||||
@ -227,7 +279,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
|
|||||||
name='BB upper',
|
name='BB upper',
|
||||||
fill="tonexty",
|
fill="tonexty",
|
||||||
fillcolor="rgba(0,176,246,0.2)",
|
fillcolor="rgba(0,176,246,0.2)",
|
||||||
line={'color': "transparent"},
|
line={'color': 'rgba(255,255,255,0)'},
|
||||||
)
|
)
|
||||||
fig.append_trace(bb_lower, 1, 1)
|
fig.append_trace(bb_lower, 1, 1)
|
||||||
fig.append_trace(bb_upper, 1, 1)
|
fig.append_trace(bb_upper, 1, 1)
|
||||||
@ -299,11 +351,17 @@ def plot_parse_args(args: List[str]) -> Namespace:
|
|||||||
default='macd',
|
default='macd',
|
||||||
dest='indicators2',
|
dest='indicators2',
|
||||||
)
|
)
|
||||||
|
arguments.parser.add_argument(
|
||||||
|
'--plot-limit',
|
||||||
|
help='Specify tick limit for plotting - too high values cause huge files - '
|
||||||
|
'Default: %(default)s',
|
||||||
|
dest='plot_limit',
|
||||||
|
default=750,
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
arguments.common_args_parser()
|
arguments.common_args_parser()
|
||||||
arguments.optimizer_shared_options(arguments.parser)
|
arguments.optimizer_shared_options(arguments.parser)
|
||||||
arguments.backtesting_options(arguments.parser)
|
arguments.backtesting_options(arguments.parser)
|
||||||
|
|
||||||
return arguments.parse_args()
|
return arguments.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,9 +26,8 @@ import plotly.graph_objs as go
|
|||||||
|
|
||||||
from freqtrade.arguments import Arguments
|
from freqtrade.arguments import Arguments
|
||||||
from freqtrade.configuration import Configuration
|
from freqtrade.configuration import Configuration
|
||||||
from freqtrade.analyze import Analyze
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
|
from freqtrade.strategy.resolver import StrategyResolver
|
||||||
import freqtrade.optimize as optimize
|
import freqtrade.optimize as optimize
|
||||||
import freqtrade.misc as misc
|
import freqtrade.misc as misc
|
||||||
|
|
||||||
@ -87,7 +86,8 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
|
|
||||||
# Init strategy
|
# Init strategy
|
||||||
try:
|
try:
|
||||||
analyze = Analyze({'strategy': config.get('strategy')})
|
strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
|
||||||
@ -113,7 +113,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
filter_pairs = config['exchange']['pair_whitelist']
|
filter_pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
tick_interval = analyze.strategy.ticker_interval
|
tick_interval = strategy.ticker_interval
|
||||||
pairs = config['exchange']['pair_whitelist']
|
pairs = config['exchange']['pair_whitelist']
|
||||||
|
|
||||||
if filter_pairs:
|
if filter_pairs:
|
||||||
@ -127,7 +127,7 @@ def plot_profit(args: Namespace) -> None:
|
|||||||
refresh_pairs=False,
|
refresh_pairs=False,
|
||||||
timerange=timerange
|
timerange=timerange
|
||||||
)
|
)
|
||||||
dataframes = analyze.tickerdata_to_dataframe(tickers)
|
dataframes = strategy.tickerdata_to_dataframe(tickers)
|
||||||
|
|
||||||
# NOTE: the dataframes are of unequal length,
|
# NOTE: the dataframes are of unequal length,
|
||||||
# 'dates' is an merged date array of them all.
|
# 'dates' is an merged date array of them all.
|
||||||
|
3
setup.py
3
setup.py
@ -18,7 +18,7 @@ setup(name='freqtrade',
|
|||||||
license='GPLv3',
|
license='GPLv3',
|
||||||
packages=['freqtrade'],
|
packages=['freqtrade'],
|
||||||
scripts=['bin/freqtrade'],
|
scripts=['bin/freqtrade'],
|
||||||
setup_requires=['pytest-runner'],
|
setup_requires=['pytest-runner', 'numpy'],
|
||||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ccxt',
|
'ccxt',
|
||||||
@ -36,6 +36,7 @@ setup(name='freqtrade',
|
|||||||
'tabulate',
|
'tabulate',
|
||||||
'cachetools',
|
'cachetools',
|
||||||
'coinmarketcap',
|
'coinmarketcap',
|
||||||
|
'scikit-optimize',
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
@ -12,11 +12,13 @@ import numpy # noqa
|
|||||||
|
|
||||||
# This class is a sample. Feel free to customize it.
|
# This class is a sample. Feel free to customize it.
|
||||||
class TestStrategy(IStrategy):
|
class TestStrategy(IStrategy):
|
||||||
|
__test__ = False # pytest expects to find tests here because of the name
|
||||||
"""
|
"""
|
||||||
This is a test strategy to inspire you.
|
This is a test strategy to inspire you.
|
||||||
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
|
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
|
||||||
|
|
||||||
You can:
|
You can:
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
- Rename the class name (Do not forget to update class_name)
|
- Rename the class name (Do not forget to update class_name)
|
||||||
- Add any methods you want to build your strategy
|
- Add any methods you want to build your strategy
|
||||||
- Add any lib you need to build your strategy
|
- Add any lib you need to build your strategy
|
||||||
@ -43,13 +45,16 @@ class TestStrategy(IStrategy):
|
|||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = '5m'
|
ticker_interval = '5m'
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
||||||
Performance Note: For the best performance be frugal on the number of indicators
|
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
|
you are using. Let uncomment only the indicator you are using in your strategies
|
||||||
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
|
||||||
|
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Momentum Indicator
|
# Momentum Indicator
|
||||||
@ -210,10 +215,11 @@ class TestStrategy(IStrategy):
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> 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 populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
@ -226,10 +232,11 @@ class TestStrategy(IStrategy):
|
|||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> 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 populated with indicators
|
||||||
|
:param metadata: Additional information, like the currently traded pair
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
|
Loading…
Reference in New Issue
Block a user