from freqtrade/develop

This commit is contained in:
Nullart 2018-07-09 08:19:28 +08:00
parent 70f2aed0a7
commit e5cd756cca
119 changed files with 15166 additions and 6207 deletions

19
CONTRIBUTING.md Normal file → Executable file
View File

@ -7,7 +7,7 @@ Feel like our bot is missing a feature? We welcome your pull requests! Few point
conformant (max-line-length = 100).
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
**Before sending the PR:**
@ -42,4 +42,21 @@ pip3.6 install flake8 coveralls
flake8 freqtrade
```
We receive a lot of code that fails the `flake8` checks.
To help with that, we encourage you to install the git pre-commit
hook that will warn you when you try to commit code that fails these checks.
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
## 3. Test if all type-hints are correct
**Install packages** (If not already installed)
``` bash
pip3.6 install mypy
```
**Run mypy**
``` bash
mypy freqtrade
```

4
Dockerfile Normal file → Executable file
View File

@ -1,7 +1,7 @@
FROM python:3.6.2
FROM python:3.6.6-slim-stretch
# Install TA-lib
RUN apt-get update && apt-get -y install build-essential && apt-get clean
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \
cd ta-lib && \

0
LICENSE Normal file → Executable file
View File

0
MANIFEST.in Normal file → Executable file
View File

205
README.md Normal file → Executable file
View File

@ -1,13 +1,14 @@
# freqtrade
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/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.
![freqtrade](https://raw.githubusercontent.com/gcarq/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
## Disclaimer
This software is for educational purposes only. Do not risk money which
@ -21,33 +22,10 @@ expect.
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.
## Table of Contents
- [Features](#features)
- [Quick start](#quick-start)
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
## 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.
## Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Features
- [x] **Based on Python 3.6+**: For botting on any operating system -
@ -55,88 +33,67 @@ Windows, macOS and Linux
- [x] **Persistence**: Persistence is achieved through sqlite
- [x] **Dry-run**: Run the bot without playing money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
parameters with Hyperopts.
- [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] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
strategy parameters with real exchange data.
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram
- [x] **Display profit/loss in fiat**: Display your profit/loss in
33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary
of your profit/loss.
- [x] **Performance status report**: Provide a performance status of
your current trades.
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades.
### Exchange supported
- [x] Bittrex
- [ ] Binance
- [ ] Others
## Table of Contents
- [Quick start](#quick-start)
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.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)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
## Quick start
This quick start section is a very short explanation on how to test the
bot in dry-run. We invite you to read the
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
to ensure you understand how the bot is working.
The following steps are made for Linux/MacOS environment
**1. Clone the repo**
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
```bash
git clone git@github.com:gcarq/freqtrade.git
git clone git@github.com:freqtrade/freqtrade.git
git checkout develop
cd freqtrade
./setup.sh --install
```
**2. Create the config file**
Switch `"dry_run": true,`
```bash
cp config.json.example config.json
vi config.json
```
**3. Build your docker image and run it**
```bash
docker build -t freqtrade .
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
### Help / Slack
For any questions not covered by the documentation or for further
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).
## Documentation
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)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
### [Bugs / Issues](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please
[search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please
[create a new issue](https://github.com/gcarq/freqtrade/issues/new) and
ensure you follow the template guide so that our team can assist you as
quickly as possible.
### [Feature Requests](https://github.com/gcarq/freqtrade/labels/enhancement)
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/gcarq/freqtrade/labels/enhancement).
If it hasn't been requested, please
[create a new request](https://github.com/gcarq/freqtrade/issues/new)
and ensure you follow the template guide so that it does not get lost
in the bug reports.
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests!
Please read our
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
to understand the requirements before sending your pull-requests.
**Important:** Always create your PR against the `develop` branch, not
`master`.
## Basic Usage
### Bot commands
```bash
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
@ -149,10 +106,16 @@ positional arguments:
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-v, --verbose be verbose
--version show program's version number and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-d PATH, --datadir PATH
path to backtest data (default:
freqtrade/tests/testdata
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
@ -160,15 +123,11 @@ optional arguments:
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
```
More details on:
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your
bot. More details on our
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- `/start`: Starts the trader
- `/stop`: Stops the trader
@ -183,6 +142,48 @@ bot. More details on our
- `/help`: Show help message
- `/version`: Show version
## 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.
## Support
### Help / Slack
For any questions not covered by the documentation or for further
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).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
ensure you follow the template guide so that our team can assist you as
quickly as possible.
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
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).
If it hasn't been requested, please
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
and ensure you follow the template guide so that it does not get lost
in the bug reports.
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests!
Please read our
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
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.
**Important:** Always create your PR against the `develop` branch, not
`master`.
## Requirements
### Min hardware required

View File

@ -1,4 +1,7 @@
#!/usr/bin/env python3
from freqtrade.main import main
main()
import sys
from freqtrade.main import main, set_loggers
set_loggers()
main(sys.argv[1:])

45
config.json.example Normal file → Executable file
View File

@ -3,46 +3,45 @@
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"ticker_interval" : "5m",
"dry_run": false,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
"trailing_stop": false,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"stoploss": -0.10,
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"key": "key",
"secret": "secret",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"BTC_DOGE"
"DOGE/BTC"
]
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": {
"enabled": true,
"token": "token",
"chat_id": "chat_id"
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id"
},
"initial_state": "running",
"internals": {

61
config_full.json.example Executable file
View File

@ -0,0 +1,61 @@
{
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false,
"ticker_interval": "5m",
"trailing_stop": false,
"trailing_stop_positive": 0.005,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"DOGE/BTC"
]
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": {
"enabled": true,
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id"
},
"db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running",
"internals": {
"process_throttle_secs": 5
},
"strategy": "DefaultStrategy",
"strategy_path": "/some/folder/"
}

0
docs/assets/freqtrade-screenshot.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

190
docs/backtesting.md Normal file → Executable file
View File

@ -1,82 +1,214 @@
# Backtesting
This page explains how to validate your strategy performance by using
Backtesting.
## Table of Contents
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
- [Understand the backtesting result](#understand-the-backtesting-result)
## Test your strategy with Backtesting
Now you have good Buy and Sell strategies, you want to test it against
real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pair) from your config file
and load static tickers located in
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
If the 5 min and 1 min ticker for the crypto-currencies to test is not
already in the `testdata` folder, backtesting will download them
automatically. Testdata files will not be updated until you specify it.
The result of backtesting will confirm you if your bot as more chance to
make a profit than a loss.
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
The backtesting is very easy with freqtrade.
### Run a backtesting against the currencies listed in your config file
**With 5 min tickers (Per default)**
#### With 5 min tickers (Per default)
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation
```
**With 1 min tickers**
#### With 1 min tickers
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
```
**Reload your testdata files**
#### Update cached pairs with the latest data
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
```
**With live data (do not alter your testdata files)**
#### With live data (do not alter your testdata files)
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
```
**Using a different on-disk ticker-data source**
#### Using a different on-disk ticker-data source
```bash
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
```
For help about backtesting usage, please refer to
[Backtesting commands](#backtesting-commands).
#### With a (custom) strategy file
```bash
python3 ./freqtrade/main.py -s TestStrategy backtesting
```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
#### Exporting trades to file
```bash
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"]
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
)
```
#### Exporting trades to file specifying a custom filename
```bash
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
```
#### Running backtest with smaller testset
Use the `--timerange` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used.
Example:
```bash
python3 ./freqtrade/main.py backtesting --timerange=-200
```
#### Advanced use of timerange
Doing `--timerange=-200` will get the last 200 timeframes
from your inputdata. You can also specify specific dates,
or a range span indexed by start and stop.
The full timerange specification:
- Use last 123 tickframes of data: `--timerange=-123`
- Use first 123 tickframes of data: `--timerange=123-`
- Use tickframes from line 123 through 456: `--timerange=123-456`
- Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600`
#### Downloading new set of ticker data
To download new set of backtesting ticker data, you can use a download script.
If you are using Binance for example:
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
- update the `pairs.json` to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
Then run:
```bash
python scripts/download_backtest_data --exchange binance
```
This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`.
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10`.
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
## Understand the backtesting result
The most important in the backtesting is to understand the result.
A backtesting result will look like that:
```
====================== BACKTESTING REPORT ================================
pair buy count avg profit % total profit BTC avg duration
-------- ----------- -------------- ------------------ --------------
BTC_ETH 56 -0.67 -0.00075455 62.3
BTC_LTC 38 -0.48 -0.00036315 57.9
BTC_ETC 42 -1.15 -0.00096469 67.0
BTC_DASH 72 -0.62 -0.00089368 39.9
BTC_ZEC 45 -0.46 -0.00041387 63.2
BTC_XLM 24 -0.88 -0.00041846 47.7
BTC_NXT 24 0.68 0.00031833 40.2
BTC_POWR 35 0.98 0.00064887 45.3
BTC_ADA 43 -0.39 -0.00032292 55.0
BTC_XMR 40 -0.40 -0.00032181 47.4
TOTAL 419 -0.41 -0.00348593 52.9
======================================== BACKTESTING REPORT =========================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
====================================== LEFT OPEN TRADES REPORT ======================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
```
The 1st table will contain all trades the bot made.
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
These trades are also included in the first table, but are extracted separately for clarity.
The last line will give you the overall performance of your strategy,
here:
```
TOTAL 419 -0.41 -0.00348593 52.9
```
@ -92,6 +224,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
expect the bot to make more profit than 1% (because it will sell every
time a trade will reach 1%).
```json
"minimal_roi": {
"0": 0.01
@ -104,6 +237,7 @@ profit. Hence, keep in mind that your performance is a mix of your
strategies, your configuration, and the crypto-currency you have set up.
## Next step
Great, your strategy is profitable. What if the bot can give your the
optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)

128
docs/bot-optimization.md Normal file → Executable file
View File

@ -1,23 +1,73 @@
# Bot Optimization
This page explains where to customize your strategies, and add new
indicators.
## Table of Contents
- [Change your strategy](#change-your-strategy)
- [Install a custom strategy file](#install-a-custom-strategy-file)
- [Customize your strategy](#change-your-strategy)
- [Add more Indicator](#add-more-indicator)
- [Where is the default strategy](#where-is-the-default-strategy)
Since the version `0.16.0` the bot allows using custom strategy file.
## Install a custom strategy file
This is very simple. Copy paste your strategy file into the folder
`user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy
```
## Change your strategy
The bot is using buy and sell strategies to buy and sell your trades.
Both are customizable.
The bot includes a default strategy file. However, we recommend you to
use your own file to not have to lose your parameters every time the default
strategy file will be updated on Github. Put your custom strategy file
into the folder `user_data/strategies`.
A strategy file contains all the information needed to build a good strategy:
- Buy strategy rules
- Sell strategy rules
- Minimal ROI recommended
- Stoploss recommended
- Hyperopt parameter
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy`
``` bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy
```
### Specify custom strategy location
If you want to use a strategy from a different folder you can pass `--strategy-path`
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
file as reference.**
### Buy strategy
The default buy strategy is located in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73-L92).
Edit the function `populate_buy_trend()` to update your buy strategy.
Sample:
Edit the method `populate_buy_trend()` into your strategy file to
update your buy strategy.
Sample from `user_data/strategies/test_strategy.py`:
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
@ -25,14 +75,9 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['tema'] > dataframe['tema'].shift(1))
),
'buy'] = 1
@ -40,43 +85,36 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
```
### Sell strategy
The default buy strategy is located in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115)
Edit the function `populate_sell_trend()` to update your buy strategy.
Sample:
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Sample from `user_data/strategies/test_strategy.py`:
```python
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(crossed_above(dataframe['rsi'], 70)) |
(crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
(dataframe['tema'] > dataframe['blower']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
),
'sell'] = 1
return dataframe
```
## Add more Indicator
As you have seen, buy and sell strategies need indicators. You can see
the indicators in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115).
Of course you can add more indicators by extending the list contained in
the function `populate_indicators()`.
As you have seen, buy and sell strategies need indicators. You can add
more indicators by extending the list contained in
the method `populate_indicators()` from your strategy file.
Sample:
```python
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
@ -111,7 +149,25 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe
```
### Want more indicator examples
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
Then uncomment indicators you need.
### Where is the default strategy?
The default buy strategy is located in the file
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
### Further strategy ideas
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
Feel free to use any of them as inspiration for your own strategies.
We're happy to accept Pull Requests containing new Strategies to that repo.
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas.
## Next step
Now you have a perfect strategy you probably want to backtesting it.
Your next step is to learn [How to use the Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md).
Now you have a perfect strategy you probably want to backtest it.
Your next step is to learn [How to use the Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md).

117
docs/bot-usage.md Normal file → Executable file
View File

@ -9,8 +9,9 @@ it.
## Bot commands
```
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
[--dry-run-db]
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--db-url PATH]
{backtesting,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies
@ -22,19 +23,21 @@ positional arguments:
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-v, --verbose be verbose
--version show program's version number and exit
-dd PATH, --datadir PATH
Path is from where backtesting and hyperopt will load the
ticker data files (default freqdata/tests/testdata).
-c PATH, --config PATH
specify configuration file (default: config.json)
-d PATH, --datadir PATH
path to backtest data
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
--dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
BaseVolume (default: 20)
--db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default:
sqlite:///tradesv3.sqlite)
```
### How to use a different config file?
@ -45,6 +48,38 @@ default, the bot will load the file `./config.json`
python3 ./freqtrade/main.py -c path/far/far/away/config.json
```
### How to use --strategy?
This parameter will allow you to load your custom strategy class.
Per default without `--strategy` or `-s` the bot will load the
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
The bot will search your strategy file within `user_data/strategies` and `freqtrade/strategy`.
To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this parameter.
**Example:**
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it:
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy
```
If the bot does not find your strategy file, it will display in an error
message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
### How to use --strategy-path?
This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!):
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
```
#### How to install a strategy?
This is very simple. Copy paste your strategy file into the folder
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
### How to use --dynamic-whitelist?
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
on BaseVolume. This value can be changed when you run the script.
@ -66,14 +101,14 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20).
### How to use --dry-run-db?
### How to use --db-url?
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
using `--dry-run-db`. This command will use a separate database file
`tradesv3.dry_run.sqlite`
using `--db-url`. This can also be used to specify a custom database
in production mode. Example command:
```bash
python3 ./freqtrade/main.py -c config.json --dry-run-db
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
```
@ -82,21 +117,32 @@ python3 ./freqtrade/main.py -c config.json --dry-run-db
Backtesting also uses the config specified via `-c/--config`.
```
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
[-r]
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
[--export-filename EXPORTFILENAME]
optional arguments:
-h, --help show this help message and exit
-l, --live using live data
-i INT, --ticker-interval INT
specify ticker interval in minutes (default: 5)
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
--timerange TIMERANGE
specify what timerange of data to use.
-l, --live using live data
-r, --refresh-pairs-cached
refresh the pairs files in tests/testdata with
the latest data from Bittrex. Use it if you want
to run your backtesting with up-to-date data.
refresh the pairs files in tests/testdata with the
latest data from the exchange. Use it if you want to
run your backtesting with up-to-date data.
--export EXPORT export backtest results, argument are: trades Example
--export=trades
--export-filename EXPORTFILENAME
Save backtest results to this filename requires
--export to be set as well Example --export-
filename=backtest_today.json (default: backtest-
result.json
```
### How to use --refresh-pairs-cached parameter?
@ -114,26 +160,33 @@ the parameter `-l` or `--live`.
## Hyperopt commands
It is possible to use hyperopt for trading strategy optimization.
Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
located in `freqtrade/optimize/hyperopt_conf.py`.
To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy.
```
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb]
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-e INT]
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
optional arguments:
-h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
--timerange TIMERANGE specify what timerange of data to use.
-e INT, --epochs INT specify number of epochs (default: 100)
--use-mongodb parallelize evaluations with mongodb (requires mongod
in PATH)
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
Specify which parameters to hyperopt. Space separate
list. Default: all
```
## A parameter missing in the configuration?
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
in [misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L84)
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
## Next step
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/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).

108
docs/configuration.md Normal file → Executable file
View File

@ -1,12 +1,15 @@
# Configure the bot
This page explains how to configure your `config.json` file.
## Table of Contents
- [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands)
## Setup config.json
We recommend to copy and use the `config.json.example` as a template
for your bot configuration.
@ -16,32 +19,50 @@ The table below will list all configuration parameters.
|----------|---------|----------|-------------|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged.
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
| `minimal_roi` | See below | Yes | Set the threshold in percent the bot will use to sell a trade. More information below.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
| `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.
| `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.
| `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.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
| `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.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
| `telegram.token` | token | No | Your Telegram bot token. Only required is `enable` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required is `enable` is `true`.
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | No | Defines the initial application state. More information below.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
The definition of each config parameters is in
[misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L205).
The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
### Understand stake_amount
`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 account an exception is generated.
To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`.
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
### Understand minimal_roi
`minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent.
See the example below:
```
"minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative
@ -51,40 +72,84 @@ See the example below:
},
```
Most of the strategy files already include the optimal `minimal_roi`
value. This parameter is optional. If you use it, it will take over the
`minimal_roi` value from the strategy file.
### Understand stoploss
`stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional.
Most of the strategy files already include the optimal `stoploss`
value. This parameter is optional. If you use it, it will take over the
`stoploss` value from the strategy file.
### Understand trailing stoploss
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
### Understand initial_state
`initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
### Understand process_throttle_secs
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
the static list of pairs) if we should buy.
### Understand ask_last_balance
`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
price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary.
### What values for exchange.name?
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
with only Bittrex and Binance.
The bot was tested with the following exchanges:
- [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance"
Feel free to test other exchanges and submit your PR to improve the bot.
### What values for fiat_display_currency?
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram.
The valid value 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".
`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".
In addition to central bank currencies, a range of cryto currencies are supported.
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
## Switch to dry-run mode
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
bot does not engage your money. It only runs a live simulation without
creating trades.
### To switch your bot in Dry-run mode:
1. Edit your `config.json` file
2. Switch dry-run to true
2. Switch dry-run to true and specify db_url for a persistent db
```json
"dry_run": true,
"db_url": "sqlite///tradesv3.dryrun.sqlite",
```
3. Remove your Bittrex API key (change them by fake api credentials)
3. Remove your Exchange API key (change them by fake api credentials)
```json
"exchange": {
"name": "bittrex",
@ -98,19 +163,23 @@ Once you will be happy with your bot performance, you can switch it to
production mode.
## Switch to production mode
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
you run it in production mode.
### To switch your bot in production mode:
1. Edit your `config.json` file
2. Switch dry-run to false
2. Switch dry-run to false and don't forget to adapt your database URL if set
```json
"dry_run": false,
```
3. Insert your Bittrex API key (change them by fake api keys)
3. Insert your Exchange API key (change them by fake api keys)
```json
"exchange": {
"name": "bittrex",
@ -118,11 +187,10 @@ you run it in production mode.
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
...
}
```
If you have not your Bittrex API key yet,
[see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md).
```
If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
## Next step
Now you have configured your config.json, the next step is to
[start your bot](https://github.com/gcarq/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).

62
docs/faq.md Normal file → Executable file
View File

@ -2,20 +2,70 @@
#### I have waited 5 minutes, why hasn't the bot made any trades yet?!
Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient!
Depending on the buy strategy, the amount of whitelisted coins, the
situation of the market etc, it can take up to hours to find good entry
position for a trade. Be patient!
#### I have made 12 trades already, why is my total profit negative?!
I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our current algorithm does leave you on the plus side, but that is after thousands of trades and even there, you will be left with losses on specific coins that you have traded tens if not hundreds of times. We of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades.
I understand your disappointment but unfortunately 12 trades is just
not enough to say anything. If you run backtesting, you can see that our
current algorithm does leave you on the plus side, but that is after
thousands of trades and even there, you will be left with losses on
specific coins that you have traded tens if not hundreds of times. We
of course constantly aim to improve the bot but it will _always_ be a
gamble, which should leave you with modest wins on monthly basis but
you can't say much from few trades.
#### Id like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
#### Id like to change the stake amount. Can I just stop the bot with
/stop and then change the config.json and run it again?
Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again.
Not quite. Trades are persisted to a database but the configuration is
currently only read when the bot is killed and restarted. `/stop` more
like pauses. You can stop your bot, adjust settings and start it again.
#### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimizing setup. See the tutorial [[here|Testing-new-strategies-with-Hyperopt]].
That's great. We have a nice backtesting and hyperoptimizing setup. See
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
#### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
#### Is there a setting to only SELL the coins being held and not
perform anymore BUYS?
You can use the `/forcesell all` command from Telegram.
### How many epoch do I need to get a good Hyperopt result?
Per default Hyperopts without `-e` or `--epochs` parameter will only
run 100 epochs, means 100 evals of your triggers, guards, .... Too few
to find a great result (unless if you are very lucky), so you probably
have to run it for 10.000 or more. But it will take an eternity to
compute.
We recommend you to run it at least 10.000 epochs:
```bash
python3 ./freqtrade/main.py hyperopt -e 10000
```
or if you want intermediate result to see
```bash
for i in {1..100}; do python3 ./freqtrade/main.py hyperopt -e 100; done
```
#### Why it is so long to run hyperopt?
Finding a great Hyperopt results takes time.
If you wonder why it takes a while to find great hyperopt results
This answer was written during the under the release 0.15.1, when we had
:
- 8 triggers
- 9 guards: let's say we evaluate even 10 values from each
- 1 stoploss calculation: let's say we want 10 values from that too to
be evaluated
The following calculation is still very rough and not very precise
but it will give the idea. With only these triggers and guards there is
already 8*10^9*10 evaluations. A roughly total of 80 billion evals.
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
of the search space.

365
docs/hyperopt.md Normal file → Executable file
View File

@ -1,157 +1,114 @@
# Hyperopt
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
- [Prepare your Hyperopt](#prepare-hyperopt)
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers)
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file)
- [Advanced Hyperopt notions](#advanced-notions)
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
- [Solving a Mystery](#solving-a-mystery)
- [Adding New Indicators](#adding-new-indicators)
- [Execute Hyperopt](#execute-hyperopt)
- [Hyperopt with MongoDB](#hyperopt-with-mongoDB)
- [Understand the hyperopts result](#understand-the-backtesting-result)
## Prepare Hyperopt
Before we start digging in Hyperopt, we recommend you to take a look at
out Hyperopt file
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
## Prepare Hyperopting
We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
### 1. Configure your Guards and Triggers
There are two places you need to change to add a new buy strategy for
testing:
- Inside the [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L167-L207).
- Inside the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
### Configure your Guards and Triggers
There are two places you need to change to add a new buy strategy for testing:
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
There you have two different type of indicators: 1. `guards` and 2.
`triggers`.
1. Guards are conditions like "never buy if ADX < 10", or never buy if
current price is over EMA10.
There you have two different type of indicators: 1. `guards` and 2. `triggers`.
1. Guards are conditions like "never buy if ADX < 10", or "never buy if
current price is over EMA10".
2. Triggers are ones that actually trigger buy in specific moment, like
"buy when EMA5 crosses over EMA10" or buy when close price touches lower
bollinger band.
"buy when EMA5 crosses over EMA10" or "buy when close price touches lower
bollinger band".
HyperOpt will, for each eval round, pick just ONE trigger, and possibly
multiple guards. So that the constructed strategy will be something like
Hyperoptimization will, for each eval round, pick one trigger and possibly
multiple guards. The constructed strategy will be something like
"*buy exactly when close price touches lower bollinger band, BUT only if
ADX > 10*".
If you have updated the buy strategy, ie. changed the contents of
`populate_buy_trend()` method you have to update the `guards` and
`triggers` hyperopts must use.
If you have updated the buy strategy, means change the content of
`populate_buy_trend()` function you have to update the `guards` and
`triggers` hyperopts must used.
## Solving a Mystery
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.
We will start by defining a search space:
```
def indicator_space() -> List[Dimension]:
"""
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')
]
```
Above definition says: I have five parameters I want you to randomly combine
to find the best combination. Two of them are integer values (`adx-value`
and `rsi-value`) and I want you test in the range of values 20 to 40.
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.
So let's write the buy strategy using these values:
```
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'])
# TRIGGERS
if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
As for an example if your `populate_buy_trend()` function is:
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(dataframe['rsi'] < 35) &
(dataframe['adx'] > 65),
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
```
Your hyperopt file must contains `guards` to find the right value for
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
means you will need to enable/disable triggers.
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.
In our case the `SPACE` and `populate_buy_trend` in hyperopt.py file
will be 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'},
]),
}
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`.
...
def populate_buy_trend(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. At this moment 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
[freqtrade/optimize/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/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 [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
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 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']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
```
It checks if Hyperopt wants the RSI guard to be enabled for this
round `params['rsi']['enabled']` and if it is, then it will add a
condition that says RSI must be < than the value hyperopt picked
for this evaluation, that is given in the `params['rsi']['value']`.
That's it. Now you can add new parts of strategies to Hyperopt and it
will try all the combinations with all different values in the search
for best working algo.
### Add a new Indicators
If you want to test an indicator that isn't used by the bot currently,
you need to add it to
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L40-L70)
inside the `populate_indicators` function.
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
Once you have updated your hyperopt configuration you can run it.
@ -160,135 +117,79 @@ it will take time you will have the result (more than 30 mins).
We strongly recommend to use `screen` to prevent any connection loss.
```bash
python3 ./freqtrade/main.py -c config.json hyperopt
python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
```
### Execute hyperopt with different ticker-data source
If you would like to learn parameters using an alternate ticke-data that
you have on-disk, use the --datadir PATH option. Default hyperopt will
use data from directory freqtrade/tests/testdata.
The `-e` flag will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations.
### Hyperopt with MongoDB
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
executing the previous command is the execution takes a long time.
To accelerate it you can use hyperopt with MongoDB.
### Execute Hyperopt with Different Ticker-Data Source
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
use data from directory `user_data/data`.
To run hyperopt with MongoDb you will need 3 terminals.
### Running Hyperopt with Smaller Testset
Use the `--timeperiod` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used.
Example:
**Terminal 1: Start MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-mongodb.py
python3 ./freqtrade/main.py hyperopt --timeperiod -200
```
**Terminal 2: Start Hyperopt worker**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-hyperopt-worker.py
```
### Running Hyperopt with Smaller Search Space
Use the `--spaces` argument to limit the search space used by hyperopt.
Letting Hyperopt optimize everything is a huuuuge search space. Often it
might make more sense to start by just searching for initial buy algorithm.
Or maybe you just want to optimize your stoploss or roi table for that awesome
new buy strategy you have.
**Terminal 3: Start Hyperopt with MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 ./freqtrade/main.py -c config.json hyperopt --use-mongodb
```
Legal values are:
**Re-run an Hyperopt**
To re-run Hyperopt you have to delete the existing MongoDB table.
```bash
cd <freqtrade>
rm -rf .hyperopt/mongodb/
```
- `all`: optimize everything
- `buy`: just search for a new buy strategy
- `roi`: just optimize the minimal profit table for your strategy
- `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss`
## Understand the hyperopts result
Once Hyperopt is completed you can use the result to adding new buy
signal. Given 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
}
}
## Understand the Hyperopts Result
Once Hyperopt is completed you can use the result to create a new strategy.
Given the following result from hyperopt:
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 **consider** the guard "adx" (`"adx"` is `"enabled": true`)
and the best value is `15.0` (`"value": 15.0,`)
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
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...
- The buy trigger that worked best was `bb_lower`.
- You should not use ADX because `adx-enabled: False`)
- You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
You have to look inside your strategy file into `buy_strategy_generator()`
method, what those values match to.
You have to look from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200)
what those values match to.
So for example you had `adx:` with the `value: 15.0` so we would look
at `adx`-block from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179).
That translates to the following code block to
[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73)
So for example you had `rsi-value: 29.0` so we would look
at `rsi`-block, that translates to the following code block:
```
(dataframe['adx'] > 15.0)
(dataframe['rsi'] < 29.0)
```
So translating your whole hyperopt result to as the new buy-signal
would be the following:
Translating your whole hyperopt result as the new buy-signal
would then look like:
```
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
(dataframe['adx'] > 15.0) & # adx-value
(dataframe['fastd'] < 40.0) & # fastd-value
(dataframe['close'] > dataframe['open']) & # green_candle
(dataframe['rsi'] < 37.0) & # rsi-value
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
(dataframe['rsi'] < 29.0) & # rsi-value
dataframe['close'] < dataframe['bb_lowerband'] # trigger
),
'buy'] = 1
return dataframe
```
## Next step
## Next Step
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/gcarq/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).

50
docs/index.md Normal file → Executable file
View File

@ -1,4 +1,5 @@
# freqtrade documentation
Welcome to freqtrade documentation. Please feel free to contribute to
this documentation if you see it became outdated by sending us a
Pull-request. Do not hesitate to reach us on
@ -6,27 +7,28 @@ Pull-request. Do not hesitate to reach us on
if you do not find the answer to your questions.
## Table of Contents
- [Pre-requisite](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
- [Setup your Bittrex account](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
- [Setup your Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
- [Bot Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
- [Install with Docker (all platforms)](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#docker)
- [Install on Linux Ubuntu](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
- [Install on MacOS](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
- [Install on Windows](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#windows)
- [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage (Start your bot)](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md)
- [Bot commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [Backtesting commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [Hyperopt commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Bot Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
- [Change your strategy](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
- [Add more Indicator](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
- [Test your strategy with Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
- [Find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
- [Control the bot with telegram](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md)
- [Contribute to the project](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [How to contribute](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [Run tests & Check PEP8 compliance](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [FAQ](https://github.com/gcarq/freqtrade/blob/develop/docs/faq.md)
- [SQL cheatsheet](https://github.com/gcarq/freqtrade/blob/develop/docs/sql_cheatsheet.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 Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows)
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [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)
- [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)
- [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)
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)

379
docs/installation.md Normal file → Executable file
View File

@ -1,111 +1,200 @@
# Install the bot
# Installation
This page explains how to prepare your environment for running the bot.
To understand how to set up the bot please read the Bot
[Bot configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
page.
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
## Table of Contents
- [Docker Automatic Installation](#docker)
- [Linux or Mac manual Installation](#linux--mac)
- [Linux - Ubuntu 16.04](#21-linux---ubuntu-1604)
- [Linux - Other distro](#22-linux---other-distro)
- [MacOS installation](#23-macos-installation)
- [Advanced Linux ](#advanced-linux)
- [Windows manual Installation](#windows)
# Docker
* [Table of Contents](#table-of-contents)
* [Easy Installation - Linux Script](#easy-installation---linux-script)
* [Manual installation](#manual-installation)
* [Automatic Installation - Docker](#automatic-installation---docker)
* [Custom Linux MacOS Installation](#custom-installation)
- [Requirements](#requirements)
- [Linux - Ubuntu 16.04](#linux---ubuntu-1604)
- [MacOS](#macos)
- [Setup Config and virtual env](#setup-config-and-virtual-env)
* [Windows](#windows)
## Easy installation
Start by downloading Docker for your platform:
- [Mac](https://www.docker.com/products/docker#/mac)
- [Windows](https://www.docker.com/products/docker#/windows)
- [Linux](https://www.docker.com/products/docker#/linux)
<!-- /TOC -->
Once you have Docker installed, simply create the config file
(e.g. `config.json`) and then create a Docker image for `freqtrade`
using the Dockerfile in this repo.
------
## Easy Installation - Linux Script
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot.
### 1. Prepare the bot
1. Clone the git
```bash
git clone https://github.com/gcarq/freqtrade.git
$ ./setup.sh
usage:
-i,--install Install freqtrade from scratch
-u,--update Command git pull to update.
-r,--reset Hard reset your develop/master branch.
-c,--config Easy config generator (Will override your existing file).
```
2. (Optional) Checkout the develop branch
### --install
This script will install everything you need to run the bot:
* Mandatory software as: `Python3`, `ta-lib`, `wget`
* Setup your virtualenv
* Configure your `config.json` file
This script is a combination of `install script` `--reset`, `--config`
### --update
Update parameter will pull the last version of your current branch and update your virtualenv.
### --reset
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
### --config
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
## Manual installation - Linux/MacOS
The following steps are made for Linux/MacOS environment
**1. Clone the repo**
```bash
git clone git@github.com:freqtrade/freqtrade.git
git checkout develop
cd freqtrade
```
**2. Create the config file**
Switch `"dry_run": true,`
```bash
cp config.json.example config.json
vi config.json
```
**3. Build your docker image and run it**
```bash
docker build -t freqtrade .
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
------
## Automatic Installation - Docker
Start by downloading Docker for your platform:
* [Mac](https://www.docker.com/products/docker#/mac)
* [Windows](https://www.docker.com/products/docker#/windows)
* [Linux](https://www.docker.com/products/docker#/linux)
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
### 1. Prepare the Bot
#### 1.1. Clone the git repository
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
#### 1.2. (Optional) Checkout the develop branch
```bash
git checkout develop
```
3. Go into the new directory
#### 1.3. Go into the new directory
```bash
cd freqtrade
```
4. Copy `config.sample` to `config.json`
```bash
cp config.json.example config.json
```
To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page
5. Create your DB file (Optional, the bot will create it if it is missing)
```bash
# For Production
touch tradesv3.sqlite
# For Dry-run
#### 1.4. Copy `config.json.example` to `config.json`
```bash
cp -n config.json.example config.json
```
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
Production
```bash
touch tradesv3.sqlite
````
Dry-Run
```bash
touch tradesv3.dryrun.sqlite
```
### 2. Build the docker image
### 2. Build the Docker image
```bash
cd freqtrade
docker build -t freqtrade .
```
For security reasons, your configuration file will not be included in the
image, you will need to bind mount it. It is also advised to bind mount
a sqlite database file (see the "5. Run a restartable docker image"
section) to keep it between updates.
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
### 3. Verify the docker image
After build process you can verify that the image was created with:
```
### 3. Verify the Docker image
After the build process you can verify that the image was created with:
```bash
docker images
```
### 4. Run the docker image
You can run a one-off container that is immediately deleted upon exiting with
the following command (config.json must be in the current working directory):
### 4. Run the Docker image
```
docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
```bash
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
In this example, the database will be created inside the docker instance
and will be lost when you will refresh your image.
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
```bash
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396)
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
### 5. Run a restartable docker image
To run a restartable instance in the background (feel free to place your
configuration and database files wherever it feels comfortable on your
filesystem).
**5.1. Move your config file and database**
To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem).
#### 5.1. Move your config file and database
```bash
mkdir ~/.freqtrade
mv config.json ~/.freqtrade
mv tradesv3.sqlite ~/.freqtrade
```
**5.2. Run the docker image**
#### 5.2. Run the docker image
```bash
docker run -d \
--name freqtrade \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade
freqtrade --db-url sqlite:///tradesv3.sqlite
```
If you are using `dry_run=True` it's not necessary to mount
`tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you
plan to use the dry run mode with the param `--dry-run-db`.
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
### 6. Monitor your Docker instance
You can then use the following commands to monitor and manage your container:
```bash
@ -116,35 +205,59 @@ docker stop freqtrade
docker start freqtrade
```
You do not need to rebuild the image for configuration changes, it will
suffice to edit `config.json` and restart the container.
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
### 7. Backtest with docker
The following assumes that the above steps (1-4) have been completed successfully.
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
# Linux / MacOS
## 1. Requirements
``` bash
docker run -d \
--name freqtrade \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade --strategy AwsomelyProfitableStrategy backtesting
```
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details.
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example).
------
## Custom Installation
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
### Requirements
Click each one for install guide:
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/),
note the bot was not tested on Python >= 3.7.x
- [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
## 2. First install required packages
This bot require Python 3.6 and TA-LIB
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
### 2.1 Linux - Ubuntu 16.04
### Linux - Ubuntu 16.04
#### 1. Install Python 3.6, Git, and wget
**2.1.1. Install Python 3.6, Git, and wget**
```bash
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
```
**2.1.2. Install TA-LIB**
#### 2. Install TA-Lib
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
```
```bash
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz
cd ta-lib
@ -155,92 +268,120 @@ cd ..
rm -rf ./ta-lib*
```
**2.1.3. [Optional] Install MongoDB**
Install MongoDB if you plan to optimize your strategy with Hyperopt.
#### 3. Install FreqTrade
Clone the git repository:
```bash
sudo apt-get install mongodb-org
git clone https://github.com/freqtrade/freqtrade.git
```
Complete tutorial on [Digital Ocean: How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04)
### 2.2. Linux - Other distro
If you are on a different Linux OS you maybe have to adapt things like:
#### 4. Configure `freqtrade` as a `systemd` service
- package manager (for example yum instead of apt-get)
- package names
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
### 2.3. MacOS installation
After that you can start the daemon with:
**2.3.1. Install Python 3.6, git and wget**
```bash
brew install python3 git wget
systemctl --user start freqtrade
```
**2.3.2. [Optional] Install MongoDB**
Install MongoDB if you plan to optimize your strategy with Hyperopt.
For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user.
```bash
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
mkdir -p <path_freqtrade>/env/mongodb
cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
sudo loginctl enable-linger "$USER"
```
## 3. Clone the repo
The following steps are made for Linux/mac environment
1. Clone the git `git clone https://github.com/gcarq/freqtrade.git`
2. (Optional) Checkout the develop branch `git checkout develop`
### MacOS
#### 1. Install Python 3.6, git, wget and ta-lib
```bash
brew install python3 git wget ta-lib
```
#### 2. Install FreqTrade
Clone the git repository:
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
Optionally checkout the develop branch:
```bash
git checkout develop
```
### Setup Config and virtual env
#### 1. Initialize the configuration
## 4. Prepare the bot
```bash
cd freqtrade
cp config.json.example config.json
```
To edit the config please refer to [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
## 5. Setup your virtual env
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
#### 2. Setup your Python virtual environment (virtualenv)
```bash
python3.6 -m venv .env
source .env/bin/activate
pip3.6 install --upgrade pip
pip3.6 install -r requirements.txt
pip3.6 install -e .
```
## 6. Run the bot
If this is the first time you run the bot, ensure you are running it
in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
#### 3. Run the Bot
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash
python3.6 ./freqtrade/main.py -c config.json
```
### Advanced Linux
**systemd service file**
Copy `./freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`)
and update `WorkingDirectory` and `ExecStart` to match your setup.
After that you can start the daemon with:
------
## Windows
We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure).
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
If that is not available on your system, feel free to try the instructions below, which led to success for some.
### Install freqtrade manually
#### Clone the git repository
```bash
systemctl --user start freqtrade
git clone https://github.com/freqtrade/freqtrade.git
```
# Windows
We do recommend Windows users to use [Docker](#docker) this will work
much easier and smoother (also safer).
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
#### install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of inofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.17cp36cp36mwin32.whl` (make sure to use the version matching your python version)
```cmd
#copy paste config.json to \path\freqtrade-develop\freqtrade
>cd \path\freqtrade-develop
>python -m venv .env
>cd .env\Scripts
>activate.bat
>cd \path\freqtrade-develop
REM optionally install ta-lib from wheel
REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl
>pip install -r requirements.txt
>pip install -e .
>cd freqtrade
>python main.py
>python freqtrade\main.py
```
*Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)*
## Next step
Now you have an environment ready, the next step is to
[configure your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
Now you have an environment ready, the next step is
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...

87
docs/plotting.md Executable file
View File

@ -0,0 +1,87 @@
# Plotting
This page explains how to plot prices, indicator, profits.
## Table of Contents
- [Plot price and indicators](#plot-price-and-indicators)
- [Plot profit](#plot-profit)
## Installation
Plotting scripts use Plotly library. Install/upgrade it with:
```
pip install --upgrade plotly
```
At least version 2.3.0 is required.
## Plot price and indicators
Usage for the price plotter:
```
script/plot_dataframe.py [-h] [-p pair] [--live]
```
Example
```
python scripts/plot_dataframe.py -p BTC_ETH
```
The `-p` pair argument, can be used to specify what
pair you would like to plot.
**Advanced use**
To plot the current live price use the `--live` flag:
```
python scripts/plot_dataframe.py -p BTC_ETH --live
```
To plot a timerange (to zoom in):
```
python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
```
Timerange doesn't work with live data.
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
```
To plot a test strategy the strategy should have first be backtested.
The results may then be plotted with the -s argument:
```
python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
```
## Plot profit
The profit plotter show a picture with three plots:
1) Average closing price for all pairs
2) The summarized profit made by backtesting.
Note that this is not the real-world profit, but
more of an estimate.
3) Each pair individually profit
The first graph is good to get a grip of how the overall market
progresses.
The second graph will show how you algorithm works or doesnt.
Perhaps you want an algorithm that steadily makes small profits,
or one that acts less seldom, but makes big swings.
The third graph can be useful to spot outliers, events in pairs
that makes profit spikes.
Usage for the profit plotter:
```
script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
```
The `-p` pair argument, can be used to plot a single pair
Example
```
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC
```

4
docs/pre-requisite.md Normal file → Executable file
View File

@ -15,7 +15,7 @@ The only things you need is a working Telegram bot and its API token.
Below we explain how to create your Telegram Bot, and how to get your
Telegram user id.
### 1. Create your instagram bot
### 1. Create your Telegram bot
**1.1. Start a chat with https://telegram.me/BotFather**
**1.2. Send the message** `/newbot`
*BotFather response:*
@ -39,8 +39,10 @@ Use this token to access the HTTP API:
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
```
**1.6. Don't forget to start the conversation with your bot, by clicking /START button**
### 2. Get your user id
**2.1. Talk to https://telegram.me/userinfobot**
**2.2. Get your "Id", you will use it for the config parameter
`chat_id`.**

21
docs/sql_cheatsheet.md Normal file → Executable file
View File

@ -32,9 +32,12 @@ CREATE TABLE trades (
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
open_rate_requested FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
@ -56,7 +59,7 @@ SELECT * FROM trades;
```sql
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>;
```
@ -67,10 +70,22 @@ SET is_open=0, close_date='2017-12-20 03:08:45.103418', close_rate=0.19638016, c
WHERE id=31;
```
## Insert manually a new trade
```sql
INSERT
INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
```
**Example:**
```sql
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
```
## Fix wrong fees in the table
If your DB was created before
[PR#200](https://github.com/gcarq/freqtrade/pull/200) was merged
[PR#200](https://github.com/freqtrade/freqtrade/pull/200) was merged
(before 12/23/17).
```sql

48
docs/stoploss.md Executable file
View File

@ -0,0 +1,48 @@
# 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 buy turns positive,
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the
black, it will be changed to be only a 1% stop loss
This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
``` json
"trailing_stop_positive": 0.01,
```
The 0.01 would translate to a 1% stop loss, once you hit profit.

30
docs/telegram-usage.md Normal file → Executable file
View File

@ -4,7 +4,7 @@ This page explains how to command your bot with Telegram.
## Pre-requisite
To control your bot with Telegram, you need first to
[set up a Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
and add your Telegram API keys into your config file.
## Telegram commands
@ -15,7 +15,8 @@ official commands. You can ask at any moment for help with `/help`.
| Command | Default | Description |
|----------|---------|-------------|
| `/start` | | Starts the trader
| `/stop` | | Starts the trader
| `/stop` | | Stops the trader
| `/reload_conf` | | Reloads the configuration file
| `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available
@ -42,7 +43,7 @@ Below, example of Telegram message you will receive for each command.
For each open trade, the bot will send you the following message.
> **Trade ID:** `123`
> **Current Pair:** BTC_CVC
> **Current Pair:** CVC/BTC
> **Open Since:** `1 days ago`
> **Amount:** `26.64180098`
> **Open Rate:** `0.00007489`
@ -57,8 +58,8 @@ Return the status of all open trades in a table format.
```
ID Pair Since Profit
---- -------- ------- --------
67 BTC_SC 1 d 13.33%
123 BTC_CVC 1 h 12.95%
67 SC/BTC 1 d 13.33%
123 CVC/BTC 1 h 12.95%
```
## /count
@ -83,7 +84,7 @@ Return a summary of your profit/loss and performance.
> **First Trade opened:** `3 days ago`
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
> **Best Performing:** `BTC_PAY: 50.23%`
> **Best Performing:** `PAY/BTC: 50.23%`
## /forcesell <trade_id>
@ -92,11 +93,11 @@ Return a summary of your profit/loss and performance.
## /performance
Return the performance of each crypto-currency the bot has sold.
> Performance:
> 1. `BTC_RCN 57.77%`
> 2. `BTC_PAY 56.91%`
> 3. `BTC_VIB 47.07%`
> 4. `BTC_SALT 30.24%`
> 5. `BTC_STORJ 27.24%`
> 1. `RCN/BTC 57.77%`
> 2. `PAY/BTC 56.91%`
> 3. `VIB/BTC 47.07%`
> 4. `SALT/BTC 30.24%`
> 5. `STORJ/BTC 27.24%`
> ...
## /balance
@ -127,3 +128,10 @@ Day Profit BTC Profit USD
## /version
> **Version:** `0.14.3`
### using proxy with telegram
```
$ export HTTP_PROXY="http://addr:port"
$ export HTTPS_PROXY="http://addr:port"
$ freqtrade
```

0
freqtrade.service Normal file → Executable file
View File

13
freqtrade/__init__.py Normal file → Executable file
View File

@ -1,5 +1,5 @@
""" FreqTrade bot """
__version__ = '0.15.1'
__version__ = '0.17.1'
class DependencyException(BaseException):
@ -12,5 +12,14 @@ class DependencyException(BaseException):
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid.
"""
class TemporaryError(BaseException):
"""
Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time.
"""

15
freqtrade/__main__.py Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.6)
"""
import sys
from freqtrade import main
if __name__ == '__main__':
main.set_loggers()
main.main(sys.argv[1:])

448
freqtrade/analyze.py Normal file → Executable file
View File

@ -2,42 +2,69 @@
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
import logging
from datetime import timedelta
from datetime import datetime
from enum import Enum
from typing import Dict, List
from typing import Dict, List, Tuple
import arrow
import talib.abstract as ta
from pandas import DataFrame, to_datetime
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.exchange import get_ticker_history
from freqtrade import constants
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
logger = logging.getLogger(__name__)
class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """
"""
Enum to distinguish between buy and sell signals
"""
BUY = "buy"
SELL = "sell"
def parse_ticker_dataframe(ticker: list) -> DataFrame:
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
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns=columns)
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
frame.sort_values('date', inplace=True)
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(dataframe: DataFrame) -> DataFrame:
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
@ -45,271 +72,200 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
return self.strategy.populate_indicators(dataframe=dataframe)
# 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)
"""
# 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
# ------------------------------------
# Previous Bollinger bands
# Because ta.BBANDS implementation is broken with small numbers, it actually
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
# and use middle band instead.
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
"""
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['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(dataframe: 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
"""
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
),
'buy'] = 1
return self.strategy.populate_buy_trend(dataframe=dataframe)
return dataframe
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
),
'sell'] = 1
return dataframe
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 analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
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 = parse_ticker_dataframe(ticker_history)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
dataframe = populate_sell_trend(dataframe)
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(pair: str, signal: SignalType) -> bool:
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 BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise
: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 = get_ticker_history(pair)
ticker_hist = exchange.get_ticker_history(pair, interval)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False
return False, False
try:
dataframe = analyze_ticker(ticker_hist)
except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False
except Exception as ex:
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
return False
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:
return False
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'])
if signal_date < arrow.now() - timedelta(minutes=10):
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) -> 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_rate=rate, trade=trade, current_time=date,
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
result = latest[signal.value] == 1
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
return result
# 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_rate: float, trade: Trade, current_time: datetime,
current_profit: float) -> bool:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
"""
trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
if trailing_stop:
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 True
# 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.strategy.stoploss
if 'trailing_stop_positive' in self.config and current_profit > 0:
# 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"since we have profit {current_profit}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
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()}

344
freqtrade/arguments.py Executable file
View File

@ -0,0 +1,344 @@
"""
This module contains the argument manager class
"""
import argparse
import logging
import os
import re
from typing import List, NamedTuple, Optional
import arrow
from freqtrade import __version__, constants
class TimeRange(NamedTuple):
"""
NamedTuple Defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is none, don't use corresponding startvalue.
"""
starttype: Optional[str] = None
stoptype: Optional[str] = None
startts: int = 0
stopts: int = 0
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: List[str], description: str) -> None:
self.args = args
self.parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None:
self.common_args_parser()
self._build_subcommands()
def get_parsed_arg(self) -> argparse.Namespace:
"""
Return the list of arguments
:return: List[str] List of arguments
"""
if self.parsed_arg is None:
self._load_args()
self.parsed_arg = self.parse_args()
return self.parsed_arg
def parse_args(self) -> argparse.Namespace:
"""
Parses given arguments and returns an argparse Namespace instance.
"""
parsed_arg = self.parser.parse_args(self.args)
return parsed_arg
def common_args_parser(self) -> None:
"""
Parses given common arguments and returns them as a parsed object.
"""
self.parser.add_argument(
'-v', '--verbose',
help='be verbose',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.INFO,
)
self.parser.add_argument(
'--version',
action='version',
version=f'%(prog)s {__version__}'
)
self.parser.add_argument(
'-c', '--config',
help='specify configuration file (default: %(default)s)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
self.parser.add_argument(
'-d', '--datadir',
help='path to backtest data',
dest='datadir',
default=None,
type=str,
metavar='PATH',
)
self.parser.add_argument(
'-s', '--strategy',
help='specify strategy class name (default: %(default)s)',
dest='strategy',
default='DefaultStrategy',
type=str,
metavar='NAME',
)
self.parser.add_argument(
'--strategy-path',
help='specify additional strategy lookup path',
dest='strategy_path',
type=str,
metavar='PATH',
)
self.parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist'
' based on 24h BaseVolume (default: %(const)s)',
dest='dynamic_whitelist',
const=constants.DYNAMIC_WHITELIST,
type=int,
metavar='INT',
nargs='?',
)
self.parser.add_argument(
'--db-url',
help='Override trades database URL, this is useful if dry_run is enabled'
' or in custom deployments (default: %(default)s)',
dest='db_url',
default=constants.DEFAULT_DB_PROD_URL,
type=str,
metavar='PATH',
)
@staticmethod
def backtesting_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given arguments for Backtesting scripts.
"""
parser.add_argument(
'-l', '--live',
help='using live data',
action='store_true',
dest='live',
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your backtesting with up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
parser.add_argument(
'--export',
help='export backtest results, argument are: trades\
Example --export=trades',
type=str,
default=None,
dest='export',
)
parser.add_argument(
'--export-filename',
help='Save backtest results to this filename \
requires --export to be set as well\
Example --export-filename=user_data/backtest_data/backtest_today.json\
(default: %(default)s)',
type=str,
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
)
@staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given common arguments for Backtesting and Hyperopt scripts.
:param parser:
:return:
"""
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
dest='ticker_interval',
type=str,
)
parser.add_argument(
'--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='realistic_simulation',
)
parser.add_argument(
'--timerange',
help='specify what timerange of data to use.',
default=None,
type=str,
dest='timerange',
)
@staticmethod
def hyperopt_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given arguments for Hyperopt scripts.
"""
parser.add_argument(
'-e', '--epochs',
help='specify number of epochs (default: %(default)d)',
dest='epochs',
default=constants.HYPEROPT_EPOCH,
type=int,
metavar='INT',
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \
Default: %(default)s',
choices=['all', 'buy', 'roi', 'stoploss'],
default='all',
nargs='+',
dest='spaces',
)
def _build_subcommands(self) -> None:
"""
Builds and attaches all subcommands
:return: None
"""
from freqtrade.optimize import backtesting, hyperopt
subparsers = self.parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
self.optimizer_shared_options(backtesting_cmd)
self.backtesting_options(backtesting_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
self.optimizer_shared_options(hyperopt_cmd)
self.hyperopt_options(hyperopt_cmd)
@staticmethod
def parse_timerange(text: Optional[str]) -> TimeRange:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)
def scripts_options(self) -> None:
"""
Parses given arguments for scripts.
"""
self.parser.add_argument(
'-p', '--pair',
help='Show profits for only this pairs. Pairs are comma-separated.',
dest='pair',
default=None
)
def testdata_dl_options(self) -> None:
"""
Parses given arguments for testdata download
"""
self.parser.add_argument(
'--pairs-file',
help='File containing a list of pairs to download',
dest='pairs_file',
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--export',
help='Export files to given dir',
dest='export',
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--days',
help='Download data for number of days',
dest='days',
type=int,
metavar='INT',
default=None
)
self.parser.add_argument(
'--exchange',
help='Exchange name (default: %(default)s)',
dest='exchange',
type=str,
default='bittrex'
)
self.parser.add_argument(
'-t', '--timeframes',
help='Specify which tickers to download. Space separated list. \
Default: %(default)s',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'],
nargs='+',
dest='timeframes',
)
self.parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes',
dest='erase',
action='store_true'
)

243
freqtrade/configuration.py Executable file
View File

@ -0,0 +1,243 @@
"""
This module contains the configuration class
"""
import json
import logging
import os
from argparse import Namespace
from typing import Any, Dict, Optional
import ccxt
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import OperationalException, constants
logger = logging.getLogger(__name__)
class Configuration(object):
"""
Class to read and init the bot configuration
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
"""
def __init__(self, args: Namespace) -> None:
self.args = args
self.config: Optional[Dict[str, Any]] = None
def load_config(self) -> Dict[str, Any]:
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
logger.info('Using config: %s ...', self.args.config)
config = self._load_config_file(self.args.config)
# Set strategy if not specified in config and or if it's non default
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.strategy})
if self.args.strategy_path:
config.update({'strategy_path': self.args.strategy_path})
# Load Common configuration
config = self._load_common_config(config)
# Load Backtesting
config = self._load_backtesting_config(config)
# Load Hyperopt
config = self._load_hyperopt_config(config)
return config
def _load_config_file(self, path: str) -> Dict[str, Any]:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
try:
with open(path) as file:
conf = json.load(file)
except FileNotFoundError:
raise OperationalException(
f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.')
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')
return self._validate_config(conf)
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load common configuration
:return: configuration as dictionary
"""
# Log level
if 'loglevel' in self.args and self.args.loglevel:
config.update({'loglevel': self.args.loglevel})
logging.basicConfig(
level=config['loglevel'],
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger.info('Log level set to %s', logging.getLevelName(config['loglevel']))
# Add dynamic_whitelist if found
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
logger.info(
'Parameter --dynamic-whitelist detected. '
'Using dynamically generated whitelist. '
'(not applicable with Backtesting and Hyperopt)'
)
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
logger.info(f'Using DB: "{config["db_url"]}"')
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
exchange_name = config.get('exchange', {}).get('name').lower()
default_path = os.path.join('user_data', 'data', exchange_name)
if not os.path.isdir(default_path):
os.makedirs(default_path)
logger.info(f'Created data directory: {default_path}')
return default_path
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Backtesting configuration
:return: configuration as dictionary
"""
# If -i/--ticker-interval is used we override the configuration parameter
# (that will override the strategy configuration)
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
logger.info('Parameter -i/--ticker-interval detected ...')
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
# If -l/--live is used we add it to the configuration
if 'live' in self.args and self.args.live:
config.update({'live': True})
logger.info('Parameter -l/--live detected ...')
# If --realistic-simulation is used we add it to the configuration
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
config.update({'realistic_simulation': True})
logger.info('Parameter --realistic-simulation detected ...')
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# If --timerange is used we add it to the configuration
if 'timerange' in self.args and self.args.timerange:
config.update({'timerange': self.args.timerange})
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
# If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir})
else:
config.update({'datadir': self._create_default_datadir(config)})
logger.info('Using data folder: %s ...', config.get('datadir'))
# If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
config.update({'refresh_pairs': True})
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
# If --export is used we add it to the configuration
if 'export' in self.args and self.args.export:
config.update({'export': self.args.export})
logger.info('Parameter --export detected: %s ...', self.args.export)
# If --export-filename is used we add it to the configuration
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
config.update({'exportfilename': self.args.exportfilename})
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
return config
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Hyperopt configuration
:return: configuration as dictionary
"""
# If --realistic-simulation is used we add it to the configuration
if 'epochs' in self.args and self.args.epochs:
config.update({'epochs': self.args.epochs})
logger.info('Parameter --epochs detected ...')
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
# If --spaces is used we add it to the configuration
if 'spaces' in self.args and self.args.spaces:
config.update({'spaces': self.args.spaces})
logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
return config
def _validate_config(self, conf: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate the configuration follow the Config Schema
:param conf: Config in JSON format
:return: Returns the config if valid, otherwise throw an exception
"""
try:
validate(conf, constants.CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.critical(
'Invalid configuration. See config.json.example. Reason: %s',
exception
)
raise ValidationError(
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
)
def get_config(self) -> Dict[str, Any]:
"""
Return the config. Use this method to get the bot config
:return: Dict: Bot config
"""
if self.config is None:
self.config = self.load_config()
return self.config
def check_exchange(self, config: Dict[str, Any]) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:return: True or raised an exception if the exchange if not supported
"""
exchange = config.get('exchange', {}).get('name').lower()
if exchange not in ccxt.exchanges:
exception_msg = f'Exchange "{exchange}" not supported.\n' \
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
logger.critical(exception_msg)
raise OperationalException(
exception_msg
)
logger.debug('Exchange "%s" supported', exchange)
return True

152
freqtrade/constants.py Executable file
View File

@ -0,0 +1,152 @@
# pragma pylint: disable=too-few-public-methods
"""
bot constants
"""
DYNAMIC_WHITELIST = 20 # pairs
PROCESS_THROTTLE_SECS = 5 # sec
TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
TICKER_INTERVAL_MINUTES = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
}
SUPPORTED_FIAT = [
"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",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
]
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': {
"type": ["number", "string"],
"minimum": 0.0005,
"pattern": UNLIMITED_STAKE_AMOUNT
},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 3},
'sell': {'type': 'number', 'minimum': 10}
}
},
'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'required': ['ask_last_balance']
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'},
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
}
},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'},
'interval': {'type': 'integer'}
}
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['exchange']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'bid_strategy',
'telegram'
]
}

430
freqtrade/exchange/__init__.py Normal file → Executable file
View File

@ -1,185 +1,417 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum
import logging
from random import randint
from typing import List, Dict, Any, Optional
from datetime import datetime
import ccxt
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
logger = logging.getLogger(__name__)
# Current selected exchange
_API: Exchange = None
_CONF: dict = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
API_RETRY_COUNT = 4
class Exchanges(enum.Enum):
"""
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
# Urls to exchange markets, insert quote and base with .format()
_EXCHANGE_URLS = {
ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}',
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}'
}
def init(config: dict) -> None:
def retrier(f):
def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
class Exchange(object):
# Current selected exchange
_api: ccxt.Exchange = None
_conf: Dict = {}
_cached_ticker: Dict[str, Any] = {}
# Holds all open sell orders for dry_run
_dry_run_open_orders: Dict[str, Any] = {}
def __init__(self, config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, _API
_CONF.update(config)
self._conf.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
self._api = self._init_ccxt(exchange_config)
# Find matching class for the given exchange name
name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config)
logger.info('Using Exchange "%s"', self.name)
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
self.validate_pairs(config['exchange']['pair_whitelist'])
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
"""
Initialize ccxt with given config and return valid
ccxt instance.
"""
# Find matching class for the given exchange name
name = exchange_config['name']
def validate_pairs(pairs: List[str]) -> None:
if name not in ccxt.exchanges:
raise OperationalException(f'Exchange {name} is not supported')
try:
api = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
'enableRateLimit': True,
})
except (KeyError, AttributeError):
raise OperationalException(f'Exchange {name} is not supported')
return api
@property
def name(self) -> str:
"""exchange Name (from ccxt)"""
return self._api.name
@property
def id(self) -> str:
"""exchange ccxt id"""
return self._api.id
def validate_pairs(self, pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises OperationalException if one pair is not available.
:param pairs: list of pairs
:return: None
"""
try:
markets = _API.get_markets()
except requests.exceptions.RequestException as e:
markets = self._api.load_markets()
except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = _CONF['stake_currency']
stake_cur = self._conf['stake_currency']
for pair in pairs:
if not pair.startswith(stake_cur):
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets:
raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
f'Pair {pair} is not available at {self.name}')
def exchange_has(self, endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in self._api.has and self._api.has[endpoint]
def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}'
self._dry_run_open_orders[order_id] = {
'pair': pair,
'rate': rate,
'price': rate,
'amount': amount,
'type': 'LIMIT_BUY',
'type': 'limit',
'side': 'buy',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed',
'fee': None
}
return order_id
return {'id': order_id}
return _API.buy(pair, rate, amount)
try:
return self._api.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
def sell(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']:
order_id = f'dry_run_sell_{randint(0, 10**6)}'
self._dry_run_open_orders[order_id] = {
'pair': pair,
'rate': rate,
'price': rate,
'amount': amount,
'type': 'LIMIT_SELL',
'type': 'limit',
'side': 'sell',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed'
}
return order_id
return {'id': order_id}
return _API.sell(pair, rate, amount)
try:
return self._api.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
@retrier
def get_balance(self, currency: str) -> float:
if self._conf['dry_run']:
return 999.9
return _API.get_balance(currency)
# ccxt exception is already handled by get_balances
balances = self.get_balances()
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
f'Could not get {currency} balance due to malformed exchange response: {balances}')
return balance['free']
@retrier
def get_balances(self) -> dict:
if self._conf['dry_run']:
return {}
def get_balances():
if _CONF['dry_run']:
return []
try:
balances = self._api.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return _API.get_balances()
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers(self) -> Dict:
try:
return self._api.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
return _API.get_ticker(pair, refresh)
@retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._cached_ticker.keys():
try:
data = self._api.fetch_ticker(pair)
try:
self._cached_ticker[pair] = {
'bid': float(data['bid']),
'ask': float(data['ask']),
}
except KeyError:
logger.debug("Could not cache ticker data for %s", pair)
return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
else:
logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair]
@retrier
def get_ticker_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> List[Dict]:
try:
# last item should be in the time interval [now - tick_interval, now]
till_time_ms = arrow.utcnow().shift(
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
).timestamp * 1000
# it looks as if some exchanges return cached data
# and they update it one in several minute, so 10 mins interval
# is necessary to skeep downloading of an empty array when all
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
@cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval)
data: List[Dict[Any, Any]] = []
while not since_ms or since_ms < till_time_ms:
data_part = self._api.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
# Because some exchange sort Tickers ASC and other DESC.
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last)
data_part = sorted(data_part, key=lambda x: x[0])
def cancel_order(order_id: str) -> None:
if _CONF['dry_run']:
if not data_part:
break
logger.debug('Downloaded data for %s time range [%s, %s]',
pair,
arrow.get(data_part[0][0] / 1000).format(),
arrow.get(data_part[-1][0] / 1000).format())
data.extend(data_part)
since_ms = data[-1][0] + 1
return data
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
@retrier
def cancel_order(self, order_id: str, pair: str) -> None:
if self._conf['dry_run']:
return
return _API.cancel_order(order_id)
try:
return self._api.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not cancel order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_order(order_id: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
@retrier
def get_order(self, order_id: str, pair: str) -> Dict:
if self._conf['dry_run']:
order = self._dry_run_open_orders[order_id]
order.update({
'id': order_id
})
return order
try:
return self._api.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not get order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
return _API.get_order(order_id)
@retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
if self._conf['dry_run']:
return []
if not self.exchange_has('fetchMyTrades'):
return []
try:
my_trades = self._api.fetch_my_trades(pair, since.timestamp())
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades
def get_pair_detail_url(pair: str) -> str:
return _API.get_pair_detail_url(pair)
except ccxt.NetworkError as e:
raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_pair_detail_url(self, pair: str) -> str:
try:
url_base = self._api.urls.get('www')
base, quote = pair.split('/')
def get_markets() -> List[str]:
return _API.get_markets()
return url_base + _EXCHANGE_URLS[self._api.id].format(base=base, quote=quote)
except KeyError:
logger.warning('Could not get exchange url for %s', self.name)
return ""
@retrier
def get_markets(self) -> List[dict]:
try:
return self._api.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_market_summaries() -> List[Dict]:
return _API.get_market_summaries()
@retrier
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
price=1, taker_or_maker='maker') -> float:
try:
# validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets()
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_name() -> str:
return _API.name
def get_fee() -> float:
return _API.fee
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()
def get_amount_lots(self, pair: str, amount: float) -> float:
"""
get buyable amount rounding, ..
"""
# validate that markets are loaded before trying to get fee
if not self._api.markets:
self._api.load_markets()
return self._api.amount_to_lots(pair, amount)

View File

@ -1,226 +0,0 @@
import logging
import requests
from typing import Dict, List, Optional
from bittrex.bittrex import Bittrex as _Bittrex
from bittrex.bittrex import API_V1_1, API_V2_0
from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
# API socket timeout
API_TIMEOUT = 60
def custom_requests(request_url, apisign):
"""
Set timeout for requests
"""
return requests.get(
request_url,
headers={"apisign": apisign},
timeout=API_TIMEOUT
).json()
class Bittrex(Exchange):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
def __init__(self, config: dict) -> None:
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
dispatch=custom_requests
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
dispatch=custom_requests
)
self.cached_ticker = {}
@staticmethod
def _validate_response(response) -> None:
"""
Validates the given bittrex response
and raises a ContentDecodingError if a non-fatal issue happened.
"""
temp_error_messages = [
'NO_API_RESPONSE',
'MIN_TRADE_REQUIREMENT_NOT_MET',
]
if response['message'] in temp_error_messages:
raise ContentDecodingError('Got {}'.format(response['message']))
@property
def fee(self) -> float:
# 0.25 %: See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self.cached_ticker.keys():
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
if not data.get('result') \
or not data['result'].get('Bid') \
or not data['result'].get('Ask') \
or not data['result'].get('Last'):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
# Update the pair
self.cached_ticker[pair] = {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
return self.cached_ticker[pair]
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
interval = 'oneMin'
elif tick_interval == 5:
interval = 'fiveMin'
else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
raise ContentDecodingError('{message} params=({pair})'.format(
message='Required property {} not present in response'.format(prop),
pair=pair))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

@ -1,172 +0,0 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class Exchange(ABC):
@property
def name(self) -> str:
"""
Name of the exchange.
:return: str representation of the class name
"""
return self.__class__.__name__
@property
def fee(self) -> float:
"""
Fee for placing an order
:return: percentage in float
"""
@abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
@abstractmethod
def sell(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: order_id of the placed sell order
"""
@abstractmethod
def get_balance(self, currency: str) -> float:
"""
Gets account balance.
:param currency: Currency as str, format: BTC
:return: float
"""
@abstractmethod
def get_balances(self) -> List[dict]:
"""
Gets account balances across currencies
:return: List of dicts, format: [
{
'Currency': str,
'Balance': float,
'Available': float,
'Pending': float,
}
...
]
"""
@abstractmethod
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
"""
Gets ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:param refresh: Shall we query a new value or a cached value is enough
:return: dict, format: {
'bid': float,
'ask': float,
'last': float
}
"""
@abstractmethod
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
"""
Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC
:param tick_interval: ticker interval in minutes
:return: list, format: [
{
'O': float, (Open)
'H': float, (High)
'L': float, (Low)
'C': float, (Close)
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
"""
def get_order(self, order_id: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
:return: dict, format: {
'id': str,
'type': str,
'pair': str,
'opened': str ISO 8601 datetime,
'closed': str ISO 8601 datetime,
'rate': float,
'amount': float,
'remaining': int
}
"""
@abstractmethod
def cancel_order(self, order_id: str) -> None:
"""
Cancels order for given order_id.
:param order_id: ID as str
:return: None
"""
@abstractmethod
def get_pair_detail_url(self, pair: str) -> str:
"""
Returns the market detail url for the given pair.
:param pair: Pair as str, format: BTC_ETC
:return: URL as str
"""
@abstractmethod
def get_markets(self) -> List[str]:
"""
Returns all available markets.
:return: List of all available pairs
"""
@abstractmethod
def get_market_summaries(self) -> List[Dict]:
"""
Returns a 24h market summary for all available markets
:return: list, format: [
{
'MarketName': str,
'High': float,
'Low': float,
'Volume': float,
'Last': float,
'TimeStamp': datetime,
'BaseVolume': float,
'Bid': float,
'Ask': float,
'OpenBuyOrders': int,
'OpenSellOrders': int,
'PrevDay': float,
'Created': datetime
},
...
]
"""
@abstractmethod
def get_wallet_health(self) -> List[Dict]:
"""
Returns a list of all wallet health information
:return: list, format: [
{
'Currency': str,
'IsActive': bool,
'LastChecked': str,
'Notice': str
},
...
"""

88
freqtrade/fiat_convert.py Normal file → Executable file
View File

@ -1,12 +1,25 @@
"""
Module that define classes to convert Crypto-currency to FIAT
e.g BTC to USD
"""
import logging
import time
from typing import Dict, List
from requests.exceptions import RequestException
from coinmarketcap import Market
from freqtrade.constants import SUPPORTED_FIAT
from pymarketcap import Pymarketcap
logger = logging.getLogger(__name__)
class CryptoFiat():
class CryptoFiat(object):
"""
Object to describe what is the price of Crypto-currency in a FIAT
"""
# Constants
CACHE_DURATION = 6 * 60 * 60 # 6 hours
@ -24,7 +37,7 @@ class CryptoFiat():
self.price = 0.0
# Private attributes
self._expiration = 0
self._expiration = 0.0
self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper()
@ -48,22 +61,40 @@ class CryptoFiat():
return self._expiration - time.time() <= 0
class CryptoToFiatConverter():
# Constants
SUPPORTED_FIAT = [
"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"
]
class CryptoToFiatConverter(object):
"""
Main class to initiate Crypto to FIAT.
This object contains a list of pair Crypto, FIAT
This object is also a Singleton
"""
__instance = None
_coinmarketcap: Market = None
_cryptomap: Dict = {}
def __new__(cls):
if CryptoToFiatConverter.__instance is None:
CryptoToFiatConverter.__instance = object.__new__(cls)
try:
CryptoToFiatConverter._coinmarketcap = Market()
except BaseException:
CryptoToFiatConverter._coinmarketcap = None
return CryptoToFiatConverter.__instance
def __init__(self) -> None:
try:
self._coinmarketcap = Pymarketcap()
except BaseException:
self._coinmarketcap = None
self._pairs: List[CryptoFiat] = []
self._load_cryptomap()
self._pairs = []
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"]))
except (ValueError, RequestException) as exception:
logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: %s",
exception
)
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@ -73,6 +104,8 @@ class CryptoToFiatConverter():
:param fiat_symbol: fiat to convert to
:return: float, value in fiat of the crypto-currency amount
"""
if crypto_symbol == fiat_symbol:
return crypto_amount
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price)
@ -88,7 +121,7 @@ class CryptoToFiatConverter():
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
@ -140,7 +173,7 @@ class CryptoToFiatConverter():
fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT
return fiat in SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@ -151,13 +184,24 @@ class CryptoToFiatConverter():
"""
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# No need to convert if both crypto and fiat are the same
if crypto_symbol == fiat_symbol:
return 1.0
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0
try:
return float(
self._coinmarketcap.ticker(
currency=crypto_symbol,
currency=self._cryptomap[crypto_symbol],
convert=fiat_symbol
)['price_' + fiat_symbol.lower()]
)['data']['quotes'][fiat_symbol.upper()]['price']
)
except BaseException:
except BaseException as exception:
logger.error("Error in _find_price: %s", exception)
return 0.0

633
freqtrade/freqtradebot.py Executable file
View File

@ -0,0 +1,633 @@
"""
Freqtrade is the main module of this bot. It contains the class Freqtrade()
"""
import copy
import logging
import time
import traceback
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional
import arrow
import requests
from cachetools import TTLCache, cached
from freqtrade import (DependencyException, OperationalException,
TemporaryError, __version__, constants, persistence)
from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.state import State
logger = logging.getLogger(__name__)
class FreqtradeBot(object):
"""
Freqtrade is the main class of the bot.
This is from here the bot start its logic.
"""
def __init__(self, config: Dict[str, Any])-> None:
"""
Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config()
method to get the config dict.
"""
logger.info(
'Starting freqtrade %s',
__version__,
)
# Init bot states
self.state = State.STOPPED
# Init objects
self.config = config
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc: RPCManager = RPCManager(self)
self.persistence = None
self.exchange = Exchange(self.config)
self._init_modules()
def _init_modules(self) -> None:
"""
Initializes all modules and updates the config
:return: None
"""
# Initialize all modules
persistence.init(self.config)
# Set initial application state
initial_state = self.config.get('initial_state')
if initial_state:
self.state = State[initial_state.upper()]
else:
self.state = State.STOPPED
def cleanup(self) -> None:
"""
Cleanup pending resources on an already stopped bot
:return: None
"""
logger.info('Cleaning up modules ...')
self.rpc.cleanup()
persistence.cleanup()
def worker(self, old_state: State = None) -> State:
"""
Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call
:return: current service state
"""
# Log state transition
state = self.state
if state != old_state:
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
logger.info('Changing state to: %s', state.name)
if state == State.STOPPED:
time.sleep(1)
elif state == State.RUNNING:
min_secs = self.config.get('internals', {}).get(
'process_throttle_secs',
constants.PROCESS_THROTTLE_SECS
)
nb_assets = self.config.get('dynamic_whitelist', None)
self._throttle(func=self._process,
min_secs=min_secs,
nb_assets=nb_assets)
return state
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
"""
Throttles the given callable that it
takes at least `min_secs` to finish execution.
:param func: Any callable
:param min_secs: minimum execution time in seconds
:return: Any
"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
duration = max(min_secs - (end - start), 0.0)
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
time.sleep(duration)
return result
def _process(self, nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if one or more trades has been created or closed, False otherwise
"""
state_changed = False
try:
# Refresh whitelist based on wallet maintenance
sanitized_list = self._refresh_whitelist(
self._gen_pair_whitelist(
self.config['stake_currency']
) if nb_assets else self.config['exchange']['pair_whitelist']
)
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
self.config['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
# First process current opened trades
for trade in trades:
state_changed |= self.process_maybe_execute_sell(trade)
# Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']:
state_changed = self.process_maybe_execute_buy()
if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders
self.check_handle_timedout()
Trade.session.flush()
except TemporaryError as error:
logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(constants.RETRY_TIMEOUT)
except OperationalException:
tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.'
self.rpc.send_msg(
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
)
logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED
return state_changed
@cached(TTLCache(maxsize=1, ttl=1800))
def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'quoteVolume')
:return: List of pairs
"""
if not self.exchange.exchange_has('fetchTickers'):
raise OperationalException(
'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot'
)
tickers = self.exchange.get_tickers()
# check length so that we make sure that '/' is actually in the string
tickers = [v for k, v in tickers.items()
if len(k.split('/')) == 2 and k.split('/')[1] == base_currency]
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
pairs = [s['symbol'] for s in sorted_tickers]
return pairs
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
"""
Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
trade
:return: the list of pairs the user wants to trade without the one unavailable or
black_listed
"""
sanitized_whitelist = whitelist
markets = self.exchange.get_markets()
markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
known_pairs = set()
for market in markets:
pair = market['symbol']
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not market['active']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist. Market is not active.',
pair
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
"""
Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price
"""
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def _get_trade_stake_amount(self) -> Optional[float]:
stake_amount = self.config['stake_amount']
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
if open_trades >= self.config['max_open_trades']:
logger.warning('Can\'t open a new trade: max number of trades is reached')
return None
return avaliable_amount / (self.config['max_open_trades'] - open_trades)
# Check if stake_amount is fulfilled
if avaliable_amount < stake_amount:
raise DependencyException(
'Available balance(%f %s) is lower than stake amount(%f %s)' % (
avaliable_amount, self.config['stake_currency'],
stake_amount, self.config['stake_currency'])
)
return stake_amount
def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]:
markets = self.exchange.get_markets()
markets = [m for m in markets if m['symbol'] == pair]
if not markets:
raise ValueError(f'Can\'t get market information for symbol {pair}')
market = markets[0]
if 'limits' not in market:
return None
min_stake_amounts = []
limits = market['limits']
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 limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts:
return None
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
if self.analyze.get_stoploss() is not None:
amount_reserve_percent += self.analyze.get_stoploss()
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
return min(min_stake_amounts)/amount_reserve_percent
def create_trade(self) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:return: True if a trade object has been created and persisted, False otherwise
"""
interval = self.analyze.get_ticker_interval()
stake_amount = self._get_trade_stake_amount()
if not stake_amount:
return False
stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency']
exc_name = self.exchange.name
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise DependencyException('No currency pairs in whitelist')
# Pick pair based on buy signals
for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
if buy and not sell:
pair = _pair
break
else:
return False
pair_s = pair.replace('_', '/')
pair_url = self.exchange.get_pair_detail_url(pair)
# Calculate amount
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount'
f' is too small ({stake_amount} < {min_stake_amount})'
)
return False
amount = stake_amount / buy_limit
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount,
stake_currency,
fiat_currency
)
# Create trade entity and return
self.rpc.send_msg(
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
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 = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee_open=fee,
fee_close=fee,
open_rate=buy_limit,
open_rate_requested=buy_limit,
open_date=datetime.utcnow(),
exchange=self.exchange.id,
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def process_maybe_execute_buy(self) -> bool:
"""
Tries to execute a buy trade in a safe way
:return: True if executed
"""
try:
# Create entity and execute trade
if self.create_trade():
return True
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
return False
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
return False
def process_maybe_execute_sell(self, trade: Trade) -> bool:
"""
Tries to execute a sell trade
:return: True if executed
"""
try:
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
order = self.exchange.get_order(trade.open_order_id, trade.pair)
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order)
if order['amount'] != new_amount:
order['amount'] = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
except OperationalException as exception:
logger.warning("could not update trade amount: %s", exception)
trade.update(order)
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
except DependencyException as exception:
logger.warning('Unable to sell trade: %s', exception)
return False
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
Get real amount for the trade
Necessary for self.exchanges which charge fees in base currency (e.g. binance)
"""
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
return order_amount
# use fee from order-dict if possible
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
if trade.pair.startswith(order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
# Fallback to Trades
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount
amount = 0
fee_abs = 0
for exectrade in trades:
amount += exectrade['amount']
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
# only applies if fee is in quote currency!
if trade.pair.startswith(exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost']
if amount != order_amount:
logger.warning(f"amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info(f"""Applying fee on amount for {trade} \
(from {order_amount} to {real_amount}) from Trades""")
return real_amount
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
(buy, sell) = self.analyze.get_signal(self.exchange,
trade.pair, self.analyze.get_ticker_interval())
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
return True
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False
def check_handle_timedout(self) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
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():
try:
# FIXME: Somehow the query above returns results
# where the open_order_id is in fact None.
# This is probably because the record got
# updated via /forcesell in a different thread.
if not trade.open_order_id:
continue
order = self.exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException:
logger.info(
'Cannot query order for %s due to %s',
trade,
traceback.format_exc())
continue
ordertime = arrow.get(order['datetime']).datetime
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
# Check if trade is still actually open
if order['status'] == 'open':
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
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
# it is conditionally called in the
# handle_timedout_limit_sell()?
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
pair_s = trade.pair.replace('_', '/')
self.exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
return True
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
"""
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
pair_s = trade.pair.replace('_', '/')
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
self.exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
logger.info('Sell order timeout for %s.', trade)
return True
# TODO: figure out how to handle partially complete sell orders
return False
def execute_sell(self, trade: Trade, limit: float) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:return: None
"""
exc = trade.exchange
pair = trade.pair
# Execute sell and update trade record
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id
trade.close_rate_requested = limit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(limit)
pair_url = self.exchange.get_pair_detail_url(trade.pair)
gain = "profit" if fmt_exp_profit > 0 else "loss"
message = f"*{exc}:* Selling\n" \
f"*Current Pair:* [{pair}]({pair_url})\n" \
f"*Limit:* `{limit}`\n" \
f"*Amount:* `{round(trade.amount, 8)}`\n" \
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
f"*Current Rate:* `{current_rate:.8f}`\n" \
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
""
# For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
stake = self.config['stake_currency']
fiat = self.config['fiat_display_currency']
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
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:
gain = "profit" if fmt_exp_profit > 0 else "loss"
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f})`'
# Send the message
self.rpc.send_msg(message)
Trade.session.flush()

40
freqtrade/indicator_helpers.py Executable file
View File

@ -0,0 +1,40 @@
from math import cos, exp, pi, sqrt
import numpy as np
import talib as ta
from pandas import Series
def went_up(series: Series) -> bool:
return series > series.shift(1)
def went_down(series: Series) -> bool:
return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
magic = pi * sqrt(2) / smoothing
a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic)
coeff3 = -a1 * a1
coeff1 = (1 - coeff2 - coeff3) / 2
filtered = series.copy()
for i in range(2, len(series)):
filtered.iloc[i] = coeff1 * (series.iloc[i] + series.iloc[i-1]) + \
coeff2 * filtered.iloc[i-1] + coeff3 * filtered.iloc[i-2]
return filtered
def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
""" Does a smoothed fishers inverse transformation.
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
v1 = 0.1 * (series - 50)
if smoothing > 0:
v2 = ta.WMA(v1.values, timeperiod=smoothing)
else:
v2 = v1
return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)

View File

@ -1,472 +1,93 @@
#!/usr/bin/env python3
import copy
import json
"""
Main Freqtrade bot script.
Read the documentation to know what cli arguments you need.
"""
import logging
import sys
import time
import traceback
from datetime import datetime
from typing import Dict, List, Optional
from argparse import Namespace
from typing import List
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade import (DependencyException, OperationalException, __version__,
exchange, persistence, rpc)
from freqtrade.analyze import SignalType, get_signal
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import (State, get_state, load_config, parse_args,
throttle, update_state)
from freqtrade.persistence import Trade
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State
logger = logging.getLogger('freqtrade')
_CONF = {}
def refresh_whitelist(whitelist: List[str]) -> List[str]:
def main(sysargv: List[str]) -> None:
"""
Check wallet health and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to trade
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
known_pairs = set()
for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def _process(nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if a trade has been created or closed, False otherwise
"""
state_changed = False
try:
# Refresh whitelist based on wallet maintenance
sanitized_list = refresh_whitelist(
gen_pair_whitelist(
_CONF['stake_currency']
) if nb_assets else _CONF['exchange']['pair_whitelist']
)
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
_CONF['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']:
try:
# Create entity and execute trade
state_changed = create_trade(float(_CONF['stake_amount']))
if not state_changed:
logger.info(
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
for trade in trades:
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
state_changed = handle_trade(trade) or state_changed
if 'unfilledtimeout' in _CONF:
# Check and handle any timed out open orders
check_handle_timedout(_CONF['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
logger.warning(
'Got %s in _process(), retrying in 30 seconds...',
error
)
time.sleep(30)
except OperationalException:
rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
))
logger.exception('Got OperationalException. Stopping trader ...')
update_state(State.STOPPED)
return state_changed
def check_handle_timedout(timeoutvalue: int) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
This function will initiate the bot and start the trading loop.
:return: None
"""
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
order = exchange.get_order(trade.open_order_id)
ordertime = arrow.get(order['opened'])
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
# Buy timeout - cancel order
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
else:
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
# Sell timeout - cancel order and update trade
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
logger.info('Sell order timeout for %s.', trade)
return True
else:
# TODO: figure out how to handle partially complete sell orders
pass
def execute_sell(trade: Trade, limit: float) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:return: None
"""
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
exchange=trade.exchange,
pair=trade.pair.replace('_', '/'),
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit
arguments = Arguments(
sysargv,
'Simple High Frequency Trading Bot for crypto currencies'
)
args = arguments.get_parsed_arg()
# For regular case, when the configuration exists
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=_CONF['stake_currency'],
profit_fiat=profit_fiat,
fiat=_CONF['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
rpc.send_msg(message)
Trade.session.flush()
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
:return True if bot should sell at current rate
"""
current_profit = trade.calc_profit_percent(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.')
return True
# Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
if time_diff > float(duration) and current_profit > threshold:
return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
return False
def handle_trade(trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
# Check if minimal roi has been reached
if min_roi_reached(trade, current_rate, datetime.utcnow()):
logger.debug('Executing sell due to ROI ...')
execute_sell(trade, current_rate)
return True
# Experimental: Check if sell signal has been enabled and triggered
if _CONF.get('experimental', {}).get('use_sell_signal'):
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if _CONF.get('experimental', {}).get('sell_profit_only'):
logger.debug('Checking if trade is profitable ...')
if trade.calc_profit(rate=current_rate) <= 0:
return False
logger.debug('Checking sell_signal ...')
if get_signal(trade.pair, SignalType.SELL):
logger.debug('Executing sell due to sell signal ...')
execute_sell(trade, current_rate)
return True
return False
def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = _CONF['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:return: True if a trade object has been created and persisted, False otherwise
"""
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise DependencyException('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
if get_signal(_pair, SignalType.BUY):
pair = _pair
break
else:
return False
# Calculate amount
buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount(
stake_amount,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Create trade entity and return
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit, stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency']
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param config: config as dict
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
rpc.init(config)
persistence.init(config, db_url)
exchange.init(config)
# Set initial application state
initial_state = config.get('initial_state')
if initial_state:
update_state(State[initial_state.upper()])
else:
update_state(State.STOPPED)
@cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
def cleanup() -> None:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
rpc.cleanup()
exit(0)
def main(sysargv=sys.argv[1:]) -> None:
"""
Loads and validates the config and handles the main loop
:return: None
"""
global _CONF
args = parse_args(sysargv,
'Simple High Frequency Trading Bot for crypto currencies')
# A subcommand has been issued
# A subcommand has been issued.
# Means if Backtesting or Hyperopt have been called we exit the bot
if hasattr(args, 'func'):
args.func(args)
exit(0)
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger.info(
'Starting freqtrade %s (loglevel=%s)',
__version__,
logging.getLevelName(args.loglevel)
)
# Load and validate configuration
_CONF = load_config(args.config)
# Initialize all modules and start main loop
if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
# If the user ask for Dry run with a local DB instead of memory
if args.dry_run_db:
if _CONF.get('dry_run', False):
_CONF.update({'dry_run_db': True})
logger.info(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
)
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
return
freqtrade = None
return_code = 1
try:
init(_CONF)
old_state = None
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name)
# Load and validate configuration
config = Configuration(args).get_config()
# Init the bot
freqtrade = FreqtradeBot(config)
state = None
while 1:
state = freqtrade.worker(old_state=state)
if state == State.RELOAD_CONF:
freqtrade = reconfigure(freqtrade, args)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
nb_assets=args.dynamic_whitelist,
)
old_state = new_state
except KeyboardInterrupt:
logger.info('Got SIGINT, aborting ...')
logger.info('SIGINT received, aborting ...')
return_code = 0
except OperationalException as e:
logger.error(str(e))
return_code = 2
except BaseException:
logger.exception('Got fatal exception!')
logger.exception('Fatal exception!')
finally:
cleanup()
if freqtrade:
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
freqtrade.cleanup()
sys.exit(return_code)
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
"""
Cleans up current instance, reloads the configuration and returns the new instance
"""
# Clean up current modules
freqtrade.cleanup()
# Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg(
'*Status:* `Config reloaded {freqtrade.state.name.lower()}...`')
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__':
main()
set_loggers()
main(sys.argv[1:])

373
freqtrade/misc.py Normal file → Executable file
View File

@ -1,318 +1,91 @@
import argparse
import enum
"""
Various tool function for Freqtrade and scripts
"""
import gzip
import json
import logging
import time
import os
from typing import Any, Callable, Dict, List
import re
from datetime import datetime
from typing import Dict
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
from wrapt import synchronized
from freqtrade import __version__
import numpy as np
from pandas import DataFrame
logger = logging.getLogger(__name__)
class State(enum.Enum):
RUNNING = 0
STOPPED = 1
# Current application state
_STATE = State.STOPPED
@synchronized
def update_state(state: State) -> None:
def shorten_date(_date: str) -> str:
"""
Updates the application state
:param state: new state
:return: None
Trim the date so it fits on small screens
"""
global _STATE
_STATE = state
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
@synchronized
def get_state() -> State:
############################################
# Used by scripts #
# Matplotlib doesn't support ::datetime64, #
# so we need to convert it into ::datetime #
############################################
def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
"""
Gets the current application state
Convert an pandas-array of timestamps into
An numpy-array of datetimes
:return: numpy-array of datetime
"""
times = []
dates = dates.astype(datetime)
for index in range(0, dates.size):
date = dates[index].to_pydatetime()
times.append(date)
return np.array(times)
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
"""
Return dates from Dataframe
:param dfs: Dict with format pair: pair_data
:return: List of dates
"""
alldates = {}
for pair, pair_data in dfs.items():
dates = datesarray_to_datetimearray(pair_data['date'])
for date in dates:
alldates[date] = 1
lst = []
for date, _ in alldates.items():
lst.append(date)
arr = np.array(lst)
return np.sort(arr, axis=0)
def file_dump_json(filename, data, is_zip=False) -> None:
"""
Dump JSON data into a file
:param filename: file to create
:param data: JSON Data to save
:return:
"""
return _STATE
print(f'dumping json to "{filename}"')
if is_zip:
if not filename.endswith('.gz'):
filename = filename + '.gz'
with gzip.open(filename, 'w') as fp:
json.dump(data, fp, default=str)
else:
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
def load_config(path: str) -> Dict:
def format_ms_time(date: int) -> str:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
convert MS date to readable format.
: epoch-string in ms
"""
with open(path) as file:
conf = json.load(file)
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')
try:
validate(conf, CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
raise ValidationError(
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
)
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
"""
Throttles the given callable that it
takes at least `min_secs` to finish execution.
:param func: Any callable
:param min_secs: minimum execution time in seconds
:return: Any
"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
duration = max(min_secs - (end - start), 0.0)
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
time.sleep(duration)
return result
def common_args_parser(description: str):
"""
Parses given common arguments and returns them as a parsed object.
"""
parser = argparse.ArgumentParser(
description=description
)
parser.add_argument(
'-v', '--verbose',
help='be verbose',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.INFO,
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
)
parser.add_argument(
'-c', '--config',
help='specify configuration file (default: config.json)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
return parser
def parse_args(args: List[str], description: str):
"""
Parses given arguments and returns an argparse Namespace instance.
Returns None if a sub command has been selected and executed.
"""
parser = common_args_parser(description)
parser.add_argument(
'--dry-run-db',
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
instead of memory DB. Work only if dry_run is enabled.',
action='store_true',
dest='dry_run_db',
)
parser.add_argument(
'-dd', '--datadir',
help='path to backtest data (default freqdata/tests/testdata',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
dest='dynamic_whitelist',
const=20,
type=int,
metavar='INT',
nargs='?',
)
build_subcommands(parser)
return parser.parse_args(args)
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_cmd.add_argument(
'-l', '--live',
action='store_true',
dest='live',
help='using live data',
)
backtesting_cmd.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval',
default=5,
type=int,
metavar='INT',
)
backtesting_cmd.add_argument(
'--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='realistic_simulation',
)
backtesting_cmd.add_argument(
'-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
Use it if you want to run your backtesting with up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
hyperopt_cmd.add_argument(
'-e', '--epochs',
help='specify number of epochs (default: 100)',
dest='epochs',
default=100,
type=int,
metavar='INT',
)
hyperopt_cmd.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
hyperopt_cmd.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval',
default=5,
type=int,
metavar='INT',
)
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
'CLP', 'CNY', 'CZK', 'DKK',
'EUR', 'GBP', 'HKD', 'HUF',
'IDR', 'ILS', 'INR', 'JPY',
'KRW', 'MXN', 'MYR', 'NOK',
'NZD', 'PHP', 'PKR', 'PLN',
'RUB', 'SEK', 'SGD', 'THB',
'TRY', 'TWD', 'ZAR', 'USD']},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'required': ['ask_last_balance']
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
}
},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'}
}
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['exchange']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'minimal_roi',
'bid_strategy',
'telegram'
]
}
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')

252
freqtrade/optimize/__init__.py Normal file → Executable file
View File

@ -1,133 +1,229 @@
# pragma pylint: disable=missing-docstring
import logging
import gzip
import json
import logging
import os
from typing import Optional, List, Dict
from pandas import DataFrame
from freqtrade.exchange import get_ticker_history
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
from typing import Optional, List, Dict, Tuple, Any
import arrow
from freqtrade import misc, constants, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__)
def load_tickerdata_file(datadir, pair, ticker_interval):
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist:
return tickerlist
start_index = 0
stop_index = len(tickerlist)
if timerange.starttype == 'line':
stop_index = timerange.startts
if timerange.starttype == 'index':
start_index = timerange.startts
elif timerange.starttype == 'date':
while (start_index < len(tickerlist) and
tickerlist[start_index][0] < timerange.startts * 1000):
start_index += 1
if timerange.stoptype == 'line':
start_index = len(tickerlist) + timerange.stopts
if timerange.stoptype == 'index':
stop_index = timerange.stopts
elif timerange.stoptype == 'date':
while (stop_index > 0 and
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
stop_index -= 1
if start_index > stop_index:
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
return tickerlist[start_index:stop_index]
def load_tickerdata_file(
datadir: str, pair: str,
ticker_interval: str,
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
"""
Load a pair from file,
:return dict OR empty if unsuccesful
"""
path = make_testdata_path(datadir)
file = '{abspath}/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,
ticker_interval=ticker_interval,
)
# The file does not exist we download it
if not os.path.isfile(file):
return None
pair_s = pair.replace('/', '_')
file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
gzipfile = file + '.gz'
# Read the file, load the json
# If the file does not exist we download it when None is returned.
# If file exists, read the file, load the json
if os.path.isfile(gzipfile):
logger.debug('Loading ticker data from file %s', gzipfile)
with gzip.open(gzipfile) as tickerdata:
pairdata = json.load(tickerdata)
elif os.path.isfile(file):
logger.debug('Loading ticker data from file %s', file)
with open(file) as tickerdata:
pairdata = json.load(tickerdata)
else:
return None
if timerange:
pairdata = trim_tickerlist(pairdata, timerange)
return pairdata
def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
def load_data(datadir: str,
ticker_interval: str,
pairs: List[str],
refresh_pairs: Optional[bool] = False,
exchange: Optional[Exchange] = None,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
"""
Loads ticker history data for the given parameters
:param ticker_interval: ticker interval in minutes
:param pairs: list of pairs
:return: dict
"""
result = {}
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
# If the user force the refresh of pairs
if refresh_pairs:
logger.info('Download data for all pairs and store them in %s', datadir)
download_pairs(datadir, _pairs)
if not exchange:
raise OperationalException("Exchange needs to be initialized when "
"calling load_data with refresh_pairs=True")
download_pairs(datadir, exchange, pairs, ticker_interval, timerange=timerange)
for pair in _pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
if not pairdata:
# download the tickerdata from exchange
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
# and retry reading the pair
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
for pair in pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if pairdata:
result[pair] = pairdata
else:
logger.warning(
'No data for pair: "%s", Interval: %s. '
'Use --refresh-pairs-cached to download the data',
pair,
ticker_interval
)
return result
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data"""
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}
def make_testdata_path(datadir: str) -> str:
"""Return the path where testdata files are stored"""
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', 'tests', 'testdata'))
return datadir or os.path.abspath(
os.path.join(
os.path.dirname(__file__), '..', 'tests', 'testdata'
)
)
def download_pairs(datadir, pairs: List[str]) -> bool:
"""For each pairs passed in parameters, download 1 and 5 ticker intervals"""
def download_pairs(datadir, exchange: Exchange, pairs: List[str],
ticker_interval: str,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
"""For each pairs passed in parameters, download the ticker intervals"""
for pair in pairs:
try:
for interval in [1, 5]:
download_backtesting_testdata(datadir, pair=pair, interval=interval)
except BaseException:
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
download_backtesting_testdata(datadir,
exchange=exchange,
pair=pair,
interval=interval,
))
tick_interval=ticker_interval,
timerange=timerange)
except BaseException:
logger.info(
'Failed to download the pair: "%s", Interval: %s',
pair,
ticker_interval
)
return False
return True
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
def load_cached_data_for_updating(filename: str,
tick_interval: str,
timerange: Optional[TimeRange]) -> Tuple[
List[Any],
Optional[int]]:
"""
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
Load cached data and choose what part of the data should be updated
"""
since_ms = None
# user sets timerange, so find the start time
if timerange:
if timerange.starttype == 'date':
since_ms = timerange.startts * 1000
elif timerange.stoptype == 'line':
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file
if os.path.isfile(filename):
with open(filename, "rt") as file:
data = json.load(file)
# remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted
if data:
data.pop()
else:
data = []
if data:
if since_ms and since_ms < data[0][0]:
# the data is requested for earlier period than the cache has
# so fully redownload all the data
data = []
else:
# a part of the data was already downloaded, so
# download unexist data only
since_ms = data[-1][0] + 1
return (data, since_ms)
def download_backtesting_testdata(datadir: str,
exchange: Exchange,
pair: str,
tick_interval: str = '5m',
timerange: Optional[TimeRange] = None) -> None:
"""
Download the latest ticker intervals from the exchange for the pairs passed in parameters
The data is downloaded starting from the last correct ticker interval data that
esists in a cache. If timerange starts earlier than the data in the cache,
the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pairs: list of pairs to download
:return: bool
:param tick_interval: ticker interval
:param timerange: range of time to download
:return: None
"""
path = make_testdata_path(datadir)
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format(
pair=pair,
interval=interval,
))
filepair = pair.replace("/", "_")
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
filepair = pair.replace("-", "_")
filename = os.path.join(path, '{pair}-{interval}.json'.format(
pair=filepair,
interval=interval,
))
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
logger.info(
'Download the pair: "%s", Interval: %s',
pair,
tick_interval
)
if os.path.isfile(filename):
with open(filename, "rt") as fp:
data = json.load(fp)
logger.debug("Current Start: {}".format(data[1]['T']))
logger.debug("Current End: {}".format(data[-1:][0]['T']))
else:
data = []
logger.debug("Current Start: None")
logger.debug("Current End: None")
data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange)
new_data = get_ticker_history(pair=pair, tick_interval=int(interval))
for row in new_data:
if row not in data:
data.append(row)
logger.debug("New Start: {}".format(data[1]['T']))
logger.debug("New End: {}".format(data[-1:][0]['T']))
data = sorted(data, key=lambda data: data['T'])
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')
with open(filename, "wt") as fp:
json.dump(data, fp)
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms)
data.extend(new_data)
return True
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
misc.file_dump_json(filename, data)

414
freqtrade/optimize/backtesting.py Normal file → Executable file
View File

@ -1,57 +1,107 @@
# pragma pylint: disable=missing-docstring,W0212
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
"""
This module contains the backtesting logic
"""
import logging
from typing import Dict, Tuple
import operator
from argparse import Namespace
from datetime import datetime
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame, Series
from pandas import DataFrame
from tabulate import tabulate
import freqtrade.misc as misc
import freqtrade.optimize as optimize
from freqtrade import exchange
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached
from freqtrade.optimize import preprocess
from freqtrade import DependencyException, constants
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
open_rate: float
close_rate: float
class Backtesting(object):
"""
Backtesting class, this class contains all the logic to run a backtest
To run a backtest:
backtesting = Backtesting(config)
backtesting.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
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
self.config['exchange']['key'] = ''
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee()
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair, pair_data in data.items():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
timeframe = [
(arrow.get(min(frame.date)), arrow.get(max(frame.date)))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
def generate_text_table(
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str:
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.currency == pair]
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(),
result.duration.mean() * ticker_interval,
result.profit.sum(),
result.loss.sum()
result.profit_abs.sum(),
result.trade_duration.mean(),
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
@ -59,139 +109,261 @@ def generate_text_table(
'TOTAL',
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean() * ticker_interval,
results.profit.sum(),
results.loss.sum()
results.profit_abs.sum(),
results.trade_duration.mean(),
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False,
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame:
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)
for index, t in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame,
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0)
trade = Trade(
open_rate=buy_row.close,
open_date=buy_row.date,
stake_amount=stake_amount,
amount=stake_amount / buy_row.open,
fee_open=self.fee,
fee_close=self.fee
)
# calculate win/lose forwards from buy point
for sell_row in partial_ticker:
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
buy_signal = sell_row.buy
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
sell_row.sell):
return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
profit_abs=trade.calc_profit(rate=sell_row.close),
open_time=buy_row.date,
close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=False,
open_rate=buy_row.close,
close_rate=sell_row.close
)
if partial_ticker:
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1]
btr = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
profit_abs=trade.calc_profit(rate=sell_row.close),
open_time=buy_row.date,
close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=True,
open_rate=buy_row.close,
close_rate=sell_row.close
)
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs)
return btr
return None
def backtest(self, args: Dict) -> DataFrame:
"""
Implements backtesting functionality
:param stake_amount: btc amount to use for each trade
:param processed: a processed dictionary with format {pair, data}
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:param realistic: do we try to simulate realistic trades? (default: True)
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
Of course try to not have ugly code. By some accessor are sometime slower than functions.
Avoid, logging on this method
:param args: a dict containing:
stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame
"""
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False)
trades = []
trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend(
self.populate_buy_trend(pair_data))[headers].copy()
# 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[:, 'sell'] = ticker_data['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True)
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None
buy_subset = ticker[ticker.buy == 1][['buy', 'open', 'close', 'date', 'sell']]
for row in buy_subset.itertuples(index=True):
for index, row in enumerate(ticker):
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if realistic:
if lock_pair_until is not None and row.Index <= lock_pair_until:
if lock_pair_until is not None and row.date <= lock_pair_until:
continue
if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date
if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue
if max_open_trades > 0:
# Increase lock
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
trade = Trade(
open_rate=row.close,
open_date=row.date,
stake_amount=stake_amount,
amount=stake_amount / row.open,
fee=exchange.get_fee()
)
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args)
# calculate win/lose forwards from buy point
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
for row2 in sell_subset.itertuples(index=True):
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
if trade_entry:
lock_pair_until = trade_entry.close_time
trades.append(trade_entry)
else:
# Set lock_pair_until to end of testing period if trade could not be closed
# This happens only if the buy-signal was with the last candle
lock_pair_until = ticker_data.iloc[-1].date
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
if (sell_profit_only and current_profit_percent < 0):
continue
if min_roi_reached(trade, row2.close, row2.date) or \
(row2.sell == 1 and use_sell_signal) or \
current_profit_percent <= stoploss:
current_profit_btc = trade.calc_profit(rate=row2.close)
lock_pair_until = row2.Index
trades.append(
(
pair,
current_profit_percent,
current_profit_btc,
row2.Index - row.Index,
current_profit_btc > 0,
current_profit_btc < 0
)
)
break
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
return DataFrame.from_records(trades, columns=labels)
def start(args):
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
exchange._API = Bittrex({'key': '', 'secret': ''})
logger.info('Using config: %s ...', args.config)
config = misc.load_config(args.config)
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = config['exchange']['pair_whitelist']
if args.live:
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval,
refresh_pairs=args.refresh_pairs)
logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount'])
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange
)
if not data:
logger.critical("No data found. Terminating.")
return
# Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades']
else:
logger.info('Ignoring max_open_trades (realistic_simulation not set) ...')
max_open_trades = 0
if args.realistic_simulation:
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
max_open_trades = config['max_open_trades']
# Monkey patch config
from freqtrade import main
main._CONF = config
preprocessed = self.tickerdata_to_dataframe(data)
preprocessed = preprocess(data)
# Print timeframe
min_date, max_date = get_timeframe(preprocessed)
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
min_date, max_date = self.get_timeframe(preprocessed)
logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results
results = backtest(
stake_amount=config['stake_amount'],
processed=preprocessed,
max_open_trades=max_open_trades,
realistic=args.realistic_simulation,
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False),
stoploss=config.get('stoploss'),
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False)
results = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': self.config.get('realistic_simulation', False),
}
)
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
'\n======================================== '
'BACKTESTING REPORT'
' =========================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
logger.info(
'\n====================================== '
'LEFT OPEN TRADES REPORT'
' ======================================\n'
'%s',
self._generate_text_table(
data,
results.loc[results.open_at_end]
)
)
def setup_configuration(args: Namespace) -> Dict[str, Any]:
"""
Prepare the configuration for the backtesting
:param args: Cli args from Arguments()
:return: Configuration
"""
configuration = Configuration(args)
config = configuration.get_config()
# Ensure we do not use Exchange credentials
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
raise DependencyException('stake amount could not be "%s" for backtesting' %
constants.UNLIMITED_STAKE_AMOUNT)
return config
def start(args: Namespace) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
# Initialize configuration
config = setup_configuration(args)
logger.info('Starting freqtrade in Backtesting mode')
# Initialize backtesting object
backtesting = Backtesting(config)
backtesting.start()

597
freqtrade/optimize/hyperopt.py Normal file → Executable file
View File

@ -1,318 +1,407 @@
# pragma pylint: disable=missing-docstring,W0212,W0603
# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement
"""
This module contains the hyperopt logic
"""
import json
import logging
import sys
import pickle
import signal
import multiprocessing
import os
import sys
from argparse import Namespace
from functools import reduce
from math import exp
from operator import itemgetter
from typing import Any, Callable, Dict, List
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from hyperopt.mongoexp import MongoTrials
import talib.abstract as ta
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
from freqtrade import main # noqa
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.misc import load_config
from freqtrade.optimize.backtesting import backtest
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.vendor.qtpylib.indicators import crossed_above
# Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.optimize import load_data
from freqtrade.optimize.backtesting import Backtesting
logger = logging.getLogger(__name__)
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1100
TOTAL_TRIES = 0
_CURRENT_TRIES = 0
CURRENT_BEST_LOSS = 100
# max average trade duration in minutes
# if eval ends with higher value, we consider it a failed eval
MAX_ACCEPTED_TRADE_DURATION = 240
# this is expexted avg profit * expected trade count
# for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85
EXPECTED_MAX_PROFIT = 3.85
# Configuration and data used by hyperopt
PROCESSED = None # optimize.preprocess(optimize.load_data())
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
# Hyperopt Trials
TRIALS_FILE = os.path.join('freqtrade', 'optimize', 'hyperopt_trials.pickle')
TRIALS = Trials()
# Monkey patch config
from freqtrade import main # noqa
main._CONF = OPTIMIZE_CONFIG
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')
SPACE = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
'stoploss': hp.uniform('stoploss', -0.5, -0.02),
}
class Hyperopt(Backtesting):
"""
Hyperopt class, this class contains all the logic to run a hyperopt simulation
To run a backtest:
hyperopt = Hyperopt(config)
hyperopt.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config)
# set TARGET_TRADES to suit your number concurrent trades so its realistic
# to the number of days
self.target_trades = 600
self.total_tries = config.get('epochs', 0)
self.current_best_loss = 100
def save_trials(trials, trials_path=TRIALS_FILE):
"""Save hyperopt trials to file"""
logger.info('Saving Trials to \'{}\''.format(trials_path))
pickle.dump(trials, open(trials_path, 'wb'))
# max average trade duration in minutes
# if eval ends with higher value, we consider it a failed eval
self.max_accepted_trade_duration = 300
# this is expexted avg profit * expected trade count
# for example 3.5%, 1100 trades, self.expected_max_profit = 3.85
# check that the reported Σ% values do not exceed this!
self.expected_max_profit = 3.0
def read_trials(trials_path=TRIALS_FILE):
"""Read hyperopt trials file"""
logger.info('Reading Trials from \'{}\''.format(trials_path))
trials = pickle.load(open(trials_path, 'rb'))
os.remove(trials_path)
# Previous evaluations
self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
self.trials: List = []
def get_args(self, params):
dimensions = self.hyperopt_space()
# 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
def populate_indicators(dataframe: DataFrame) -> DataFrame:
dataframe['adx'] = ta.ADX(dataframe)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['mfi'] = ta.MFI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
def save_trials(self) -> None:
"""
Save hyperopt trials to file
"""
if self.trials:
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
dump(self.trials, self.trials_file)
def read_trials(self) -> List:
"""
Read hyperopt trials file
"""
logger.info('Reading Trials from \'%s\'', self.trials_file)
trials = load(self.trials_file)
os.remove(self.trials_file)
return trials
def log_trials_result(self) -> None:
"""
Display Best hyperopt result
"""
results = sorted(self.trials, key=itemgetter('loss'))
best_result = results[0]
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_trials_result(trials):
vals = json.dumps(trials.best_trial['misc']['vals'], indent=4)
results = trials.best_trial['result']['result']
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
def log_results(results):
""" log results if it is better than any previous evaluation """
global CURRENT_BEST_LOSS
if results['loss'] < CURRENT_BEST_LOSS:
CURRENT_BEST_LOSS = results['loss']
logger.info('{:5d}/{}: {}'.format(
results['current_tries'],
results['total_tries'],
results['result']))
def log_results(self, results) -> None:
"""
Log results if it is better than any previous evaluation
"""
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']
log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
print(log_msg)
else:
print('.', end='')
sys.stdout.flush()
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float:
"""
Objective function, returns smaller number for more optimal results
"""
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
result = trade_loss + profit_loss + duration_loss
return result
def calculate_loss(total_profit: float, trade_count: int, trade_duration: float):
""" objective function, returns smaller number for more optimal results """
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
return trade_loss + profit_loss + duration_loss
@staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
"""
Generate the ROI table thqt will be used by Hyperopt
"""
roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
return roi_table
def optimizer(params):
global _CURRENT_TRIES
@staticmethod
def roi_space() -> List[Dimension]:
"""
Values to search for each ROI steps
"""
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]
from freqtrade.optimize import backtesting
backtesting.populate_buy_trend = buy_strategy_generator(params)
@staticmethod
def stoploss_space() -> List[Dimension]:
"""
Stoploss search space
"""
return [
Real(-0.5, -0.02, name='stoploss'),
]
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss'])
result_explanation = format_results(results)
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching strategy parameters
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
total_profit = results.profit_percent.sum()
trade_count = len(results.index)
trade_duration = results.duration.mean() * 5
def has_space(self, space: str) -> bool:
"""
Tell if a space value is contained in the configuration
"""
if space in self.config['spaces'] or 'all' in self.config['spaces']:
return True
return False
if trade_count == 0 or trade_duration > MAX_ACCEPTED_TRADE_DURATION:
print('.', end='')
return {
'status': STATUS_FAIL,
'loss': float('inf')
}
def hyperopt_space(self) -> List[Dimension]:
"""
Return the space to use during Hyperopt
"""
spaces: List[Dimension] = []
if self.has_space('buy'):
spaces += Hyperopt.indicator_space()
if self.has_space('roi'):
spaces += Hyperopt.roi_space()
if self.has_space('stoploss'):
spaces += Hyperopt.stoploss_space()
return spaces
loss = calculate_loss(total_profit, trade_count, trade_duration)
_CURRENT_TRIES += 1
log_results({
'loss': loss,
'current_tries': _CURRENT_TRIES,
'total_tries': TOTAL_TRIES,
'result': result_explanation,
})
return {
'loss': loss,
'status': STATUS_OK,
'result': result_explanation,
}
def format_results(results: DataFrame):
return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC. Avg duration {:5.1f} mins.').format(
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean() * 5,
)
def buy_strategy_generator(params):
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by hyperopt
"""
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
Buy strategy Hyperopt will build and use
"""
conditions = []
# GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['uptrend_short_ema']['enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10'])
if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['mfi'] < params['mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['fastd'] < params['fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] > params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] < params['rsi-value'])
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
conditions.append(triggers.get(params['trigger']['type']))
if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['close'], dataframe['sar']
))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
def generate_optimizer(self, _params) -> Dict:
params = self.get_args(_params)
def start(args):
global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES
if self.has_space('roi'):
self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
TOTAL_TRIES = args.epochs
if self.has_space('buy'):
self.populate_buy_trend = self.buy_strategy_generator(params)
exchange._API = Bittrex({'key': '', 'secret': ''})
if self.has_space('stoploss'):
self.analyze.strategy.stoploss = params['stoploss']
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='\n%(message)s',
processed = load(TICKERDATA_PICKLE)
results = self.backtest(
{
'stake_amount': self.config['stake_amount'],
'processed': processed,
'realistic': self.config.get('realistic_simulation', False),
}
)
result_explanation = self.format_results(results)
total_profit = results.profit_percent.sum()
trade_count = len(results.index)
trade_duration = results.trade_duration.mean()
if trade_count == 0:
return {
'loss': MAX_LOSS,
'params': params,
'result': result_explanation,
}
loss = self.calculate_loss(total_profit, trade_count, trade_duration)
return {
'loss': loss,
'params': params,
'result': result_explanation,
}
def format_results(self, results: DataFrame) -> str:
"""
Return the format result in a string
"""
trades = len(results.index)
avg_profit = results.profit_percent.mean() * 100.0
total_profit = results.profit_abs.sum()
stake_cur = self.config['stake_currency']
profit = results.profit_percent.sum()
duration = 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}
)
logger.info('Using config: %s ...', args.config)
config = load_config(args.config)
pairs = config['exchange']['pair_whitelist']
PROCESSED = optimize.preprocess(optimize.load_data(
args.datadir, pairs=pairs, ticker_interval=args.ticker_interval))
def run_optimizer_parallel(self, parallel, asked) -> List:
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
if args.mongodb:
logger.info('Using mongodb ...')
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
db_name = 'freqtrade_hyperopt'
TRIALS = MongoTrials('mongo://127.0.0.1:1234/{}/jobs'.format(db_name), exp_key='exp1')
else:
logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, signal_handler)
# read trials file if we have one
if os.path.exists(TRIALS_FILE):
TRIALS = read_trials()
_CURRENT_TRIES = len(TRIALS.results)
TOTAL_TRIES = TOTAL_TRIES + _CURRENT_TRIES
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(
'Continuing with trials. Current: {}, Total: {}'
.format(_CURRENT_TRIES, TOTAL_TRIES))
try:
best_parameters = fmin(
fn=optimizer,
space=SPACE,
algo=tpe.suggest,
max_evals=TOTAL_TRIES,
trials=TRIALS
'Loaded %d previous evaluations from disk.',
len(self.trials)
)
results = sorted(TRIALS.results, key=itemgetter('loss'))
best_result = results[0]['result']
def start(self) -> None:
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = load_data(
datadir=str(self.config.get('datadir')),
pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval,
timerange=timerange
)
except ValueError:
best_parameters = {}
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
'try with more epochs (param: -e).'
if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
self.exchange = None # type: ignore
self.load_previous_results()
# Improve best parameter logging display
if best_parameters:
best_parameters = space_eval(SPACE, best_parameters)
cpus = multiprocessing.cpu_count()
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
logger.info('Best Result:\n%s', best_result)
opt = self.get_optimizer(cpus)
EVALS = max(self.total_tries//cpus, 1)
try:
with Parallel(n_jobs=cpus) as parallel:
for i in range(EVALS):
asked = opt.ask(n_points=cpus)
f_val = self.run_optimizer_parallel(parallel, asked)
opt.tell(asked, [i['loss'] for i in f_val])
# Store trials result to file to resume next time
save_trials(TRIALS)
self.trials += f_val
for j in range(cpus):
self.log_results({
'loss': f_val[j]['loss'],
'current_tries': i * cpus + j,
'total_tries': self.total_tries,
'result': f_val[j]['result'],
})
except KeyboardInterrupt:
print('User interrupted..')
self.save_trials()
self.log_trials_result()
def signal_handler(sig, frame):
"""Hyperopt SIGINT handler"""
logger.info('Hyperopt received {}'.format(signal.Signals(sig).name))
def start(args: Namespace) -> None:
"""
Start Backtesting script
:param args: Cli args from Arguments()
:return: None
"""
save_trials(TRIALS)
log_trials_result(TRIALS)
sys.exit(0)
# Remove noisy log messages
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
# Initialize configuration
# Monkey patch the configuration with hyperopt_conf.py
configuration = Configuration(args)
logger.info('Starting freqtrade in Hyperopt mode')
config = configuration.load_config()
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
# Initialize backtesting object
hyperopt = Hyperopt(config)
hyperopt.start()

View File

@ -1,41 +0,0 @@
"""
File that contains the configuration for Hyperopt
"""
def hyperopt_optimize_conf() -> dict:
"""
This function is used to define which parameters Hyperopt must used.
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
without MongoDB, Hyperopt will use the pair your have set in your config file.
:return:
"""
return {
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
"minimal_roi": {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
]
}
}

225
freqtrade/persistence.py Normal file → Executable file
View File

@ -1,51 +1,126 @@
"""
This module contains the class to persist trades into SQLite
"""
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Dict, Optional
from typing import Any, Dict, Optional
import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine)
from sqlalchemy.engine import Engine
create_engine, inspect)
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE = 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, engine: Optional[Engine] = None) -> None:
def init(config: Dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param engine: database engine for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not engine:
if _CONF.get('dry_run', False):
# the user wants dry run to use a DB
if _CONF.get('dry_run_db', False):
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
# Otherwise dry run will store in memory
else:
engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else:
engine = create_engine('sqlite:///tradesv3.sqlite')
db_url = config.get('db_url', None)
kwargs = {}
# Take care of thread ownership if in-memory db
if db_url == 'sqlite://':
kwargs.update({
'connect_args': {'check_same_thread': False},
'poolclass': StaticPool,
'echo': False,
})
try:
engine = create_engine(db_url, **kwargs)
except NoSuchModuleError:
raise OperationalException(f'Given value for db_url: \'{db_url}\' '
f'is no valid database URL! (See {_SQL_DOCS_URL})')
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine)
check_migrate(engine)
# Clean dry_run DB if the db is not in-memory
if config.get('dry_run', False) and db_url != 'sqlite://':
clean_dry_run_db()
def has_column(columns, searchname: str) -> bool:
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:
"""
Checks if migration is necessary and migrates if necessary
"""
inspector = inspect(engine)
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, 'max_rate'):
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')
# Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}")
# let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema
engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, max_rate
)
select id, lower(exchange),
case
when instr(pair, '_') != 0 then
substr(pair, instr(pair, '_') + 1) || '/' ||
substr(pair, 1, instr(pair, '_') - 1)
else pair
end
pair,
is_open, fee fee_open, fee fee_close,
open_rate, {open_rate_requested} open_rate_requested, close_rate,
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{max_rate} max_rate
from {table_back_name}
""")
# Reread columns - the above recreated the table!
inspector = inspect(engine)
cols = inspector.get_columns('trades')
def cleanup() -> None:
@ -56,31 +131,90 @@ def cleanup() -> None:
Trade.session.flush()
def clean_dry_run_db() -> None:
"""
Remove open_order_id from a Dry_run DB
:return: None
"""
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
# Check we are updating only a dry_run order not a prod one
if 'dry_run' in trade.open_order_id:
trade.open_order_id = None
class Trade(_DECL_BASE):
"""
Class used to define a trade structure
"""
__tablename__ = 'trades'
id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
fee = 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)
open_rate = Column(Float)
open_rate_requested = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
self.id,
self.pair,
self.amount,
self.open_rate,
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
)
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
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:
"""
@ -88,23 +222,24 @@ class Trade(_DECL_BASE):
:param order: order retrieved by exchange.get_order()
:return: None
"""
order_type = order['type']
# Ignore open and cancelled orders
if not order['closed'] or order['rate'] is None:
if order['status'] == 'open' or order['price'] is None:
return
logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY':
if order_type == 'limit' and order['side'] == 'buy':
# Update open rate and actual amount
self.open_rate = Decimal(order['rate'])
self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None
elif order['type'] == 'LIMIT_SELL':
self.close(order['rate'])
elif order_type == 'limit' and order['side'] == 'sell':
self.close(order['price'])
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
raise ValueError(f'Unknown order type: {order_type}')
cleanup()
def close(self, rate: float) -> None:
@ -134,7 +269,7 @@ class Trade(_DECL_BASE):
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
fees = buy_trade * Decimal(fee or self.fee_open)
return float(buy_trade + fees)
def calc_close_trade_price(
@ -155,7 +290,7 @@ class Trade(_DECL_BASE):
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
def calc_profit(
@ -172,10 +307,11 @@ class Trade(_DECL_BASE):
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
rate=(rate or self.close_rate),
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(
self,
@ -185,14 +321,15 @@ class Trade(_DECL_BASE):
Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:return: profit in percentage as float
"""
getcontext().prec = 8
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
profit_percent = (close_trade_price / open_trade_price) - 1
return float(f"{profit_percent:.8f}")

42
freqtrade/rpc/__init__.py Normal file → Executable file
View File

@ -1,42 +0,0 @@
import logging
from . import telegram
logger = logging.getLogger(__name__)
REGISTERED_MODULES = []
def init(config: dict) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
if config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
REGISTERED_MODULES.append('telegram')
telegram.init(config)
def cleanup() -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in REGISTERED_MODULES:
logger.debug('Cleaning up rpc.telegram ...')
telegram.cleanup()
def send_msg(msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
logger.info(msg)
if 'telegram' in REGISTERED_MODULES:
telegram.send_msg(msg)

391
freqtrade/rpc/rpc.py Executable file
View File

@ -0,0 +1,391 @@
"""
This module contains class to define a RPC communications
"""
import logging
from abc import abstractmethod
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Any, Dict, List, Tuple
import arrow
import sqlalchemy as sql
from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.state import State
logger = logging.getLogger(__name__)
class RPCException(Exception):
"""
Should be raised with a rpc-formatted message in an _rpc_* method
if the required state is wrong, i.e.:
raise RPCException('*Status:* `no active trade`')
"""
pass
class RPC(object):
"""
RPC class can be used to have extra feature, like bot data, and access to DB data
"""
def __init__(self, freqtrade) -> None:
"""
Initializes all enabled rpc modules
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
self._freqtrade = freqtrade
@abstractmethod
def cleanup(self) -> None:
""" Cleanup pending module resources """
@property
@abstractmethod
def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]:
"""
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`')
elif not trades:
raise RPCException('*Status:* `no active trade`')
else:
result = []
for trade in trades:
order = None
if trade.open_order_id:
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
if trade.close_profit else None)
market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair)
trade_date = arrow.get(trade.open_date).humanize()
open_rate = trade.open_rate
close_rate = trade.close_rate
amount = round(trade.amount, 8)
current_profit = round(current_profit * 100, 2)
open_order = ''
if order:
order_type = order['type']
order_side = order['side']
order_rem = order['remaining']
open_order = f'({order_type} {order_side} rem={order_rem:.8f})'
message = f"*Trade ID:* `{trade.id}`\n" \
f"*Current Pair:* [{trade.pair}]({market_url})\n" \
f"*Open Since:* `{trade_date}`\n" \
f"*Amount:* `{amount}`\n" \
f"*Open Rate:* `{open_rate:.8f}`\n" \
f"*Close Rate:* `{close_rate}`\n" \
f"*Current Rate:* `{current_rate:.8f}`\n" \
f"*Close Profit:* `{fmt_close_profit}`\n" \
f"*Current Profit:* `{current_profit:.2f}%`\n" \
f"*Open Order:* `{open_order}`"\
result.append(message)
return result
def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`')
elif not trades:
raise RPCException('*Status:* `no active order`')
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
trade_perc = (100 * trade.calc_profit_percent(current_rate))
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
f'{trade_perc:.2f}%'
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
return df_statuses
def _rpc_daily_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self._freqtrade.fiat_converter
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = {
'amount': f'{curdayprofit:.8f}',
'trades': len(trades)
}
return [
[
key,
'{value:.8f} {symbol}'.format(
value=float(value['amount']),
symbol=stake_currency
),
'{value:.3f} {symbol}'.format(
value=fiat.convert_amount(
value['amount'],
stake_currency,
fiat_display_currency
),
symbol=fiat_display_currency
),
'{value} trade{s}'.format(
value=value['trades'],
s='' if value['trades'] < 2 else 's'
),
]
for key, value in profit_days.items()
]
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" Returns cumulative profit statistics """
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate: float = 0.0
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
)
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
).filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')).first()
if not best_pair:
raise RPCException('*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self._freqtrade.fiat_converter
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
profit_closed_fiat = fiat.convert_amount(
profit_closed_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
profit_all_fiat = fiat.convert_amount(
profit_all_coin,
stake_currency,
fiat_display_currency
)
num = float(len(durations) or 1)
return {
'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2),
}
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" Returns current account balance per crypto """
output = []
total = 0.0
for coin, balance in self._freqtrade.exchange.get_balances().items():
if not balance['total']:
continue
if coin == 'BTC':
rate = 1.0
else:
if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
est_btc: float = rate * balance['total']
total = total + est_btc
output.append(
{
'currency': coin,
'available': balance['free'],
'balance': balance['total'],
'pending': balance['used'],
'est_btc': est_btc
}
)
if total == 0.0:
raise RPCException('`All balances are zero.`')
fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
return output, total, symbol, value
def _rpc_start(self) -> str:
""" Handler for start """
if self._freqtrade.state == State.RUNNING:
return '*Status:* `already running`'
self._freqtrade.state = State.RUNNING
return '`Starting trader ...`'
def _rpc_stop(self) -> str:
""" Handler for stop """
if self._freqtrade.state == State.RUNNING:
self._freqtrade.state = State.STOPPED
return '`Stopping trader ...`'
return '*Status:* `already stopped`'
def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!!
def _rpc_forcesell(self, trade_id) -> None:
"""
Handler for forcesell <id>.
Sells the given trade at current price
"""
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'buy':
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('price') or trade.open_rate)
# Do the best effort, if we don't know 'filled' amount, don't try selling
if order['filled'] is None:
return
trade.amount = order['filled']
# Ignore trades with an attached LIMIT_SELL order
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'sell':
return
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
self._freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return
# Query for trade
trade = Trade.query.filter(
sql.and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)
).first()
if not trade:
logger.warning('forcesell: Invalid argument received')
raise RPCException('Invalid argument.')
_exec_forcesell(trade)
Trade.session.flush()
def _rpc_performance(self) -> List[Dict]:
"""
Handler for performance.
Shows a performance statistic from finished trades
"""
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'),
sql.func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
return [
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
for pair, rate, count in pair_rates
]
def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
return Trade.query.filter(Trade.is_open.is_(True)).all()

44
freqtrade/rpc/rpc_manager.py Executable file
View File

@ -0,0 +1,44 @@
"""
This module contains class to manage RPC communications (Telegram, Slack, ...)
"""
import logging
from typing import List
from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__)
class RPCManager(object):
"""
Class to manage RPC objects (Telegram, Slack, ...)
"""
def __init__(self, freqtrade) -> None:
""" Initializes all enabled rpc modules """
self.registered_modules: List[RPC] = []
# Enable telegram
if freqtrade.config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade))
def cleanup(self) -> None:
""" Stops all enabled rpc modules """
logger.info('Cleaning up rpc modules ...')
while self.registered_modules:
mod = self.registered_modules.pop()
logger.debug('Cleaning up rpc.%s ...', mod.name)
mod.cleanup()
del mod
def send_msg(self, msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
logger.info('Sending rpc message: %s', msg)
for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg)

682
freqtrade/rpc/telegram.py Normal file → Executable file
View File

@ -1,65 +1,102 @@
# pragma pylint: disable=unused-argument, unused-variable, protected-access, invalid-name
"""
This module manage Telegram communication
"""
import logging
import re
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Callable
import arrow
from pandas import DataFrame
from sqlalchemy import and_, func, text
from tabulate import tabulate
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater
from freqtrade import __version__, exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import State, get_state, update_state
from freqtrade.persistence import Trade
from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC, RPCException
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_UPDATER: Updater = None
_CONF = {}
_FIAT_CONVERT = CryptoToFiatConverter()
logger.debug('Included module rpc.telegram ...')
def init(config: dict) -> None:
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(self, *args, **kwargs):
""" Decorator logic """
update = kwargs.get('update') or args[1]
# Reject unauthorized messages
chat_id = int(self._config['telegram']['chat_id'])
if int(update.message.chat_id) != chat_id:
logger.info(
'Rejected unauthorized message from: %s',
update.message.chat_id
)
return wrapper
logger.info(
'Executing handler: %s for chat_id: %s',
command_handler.__name__,
chat_id
)
try:
return command_handler(self, *args, **kwargs)
except BaseException:
logger.exception('Exception occurred within Telegram module')
return wrapper
class Telegram(RPC):
""" This class handles all telegram communication """
@property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None:
"""
Init the Telegram call, and init the super class RPC
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
super().__init__(freqtrade)
self._updater: Updater = None
self._config = freqtrade.config
self._init()
def _init(self) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
global _UPDATER
_CONF.update(config)
if not is_enabled():
return
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
CommandHandler('daily', _daily),
CommandHandler('count', _count),
CommandHandler('help', _help),
CommandHandler('version', _version),
CommandHandler('status', self._status),
CommandHandler('profit', self._profit),
CommandHandler('balance', self._balance),
CommandHandler('start', self._start),
CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell),
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('help', self._help),
CommandHandler('version', self._version),
]
for handle in handles:
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
self._updater.dispatcher.add_handler(handle)
self._updater.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
@ -70,49 +107,19 @@ def init(config: dict) -> None:
[h.command for h in handles]
)
def cleanup() -> None:
def cleanup(self) -> None:
"""
Stops all running telegram threads.
:return: None
"""
if not is_enabled():
return
_UPDATER.stop()
self._updater.stop()
def send_msg(self, msg: str) -> None:
""" Send a message to telegram channel """
self._send_msg(msg)
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(*args, **kwargs):
update = kwargs.get('update') or args[1]
# Reject unauthorized messages
chat_id = int(_CONF['telegram']['chat_id'])
if int(update.message.chat_id) != chat_id:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
return wrapper
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
try:
return command_handler(*args, **kwargs)
except BaseException:
logger.exception('Exception occurred within Telegram module')
return wrapper
@authorized_only
def _status(bot: Bot, update: Update) -> None:
@authorized_only
def _status(self, bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
@ -125,57 +132,17 @@ def _status(bot: Bot, update: Update) -> None:
params = update.message.text.replace('/status', '').split(' ') \
if update.message.text else []
if 'table' in params:
_status_table(bot, update)
self._status_table(bot, update)
return
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active trade`', bot=bot)
else:
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
send_msg(message, bot=bot)
try:
for trade_msg in self._rpc_trade_status():
self._send_msg(trade_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _status_table(bot: Bot, update: Update) -> None:
@authorized_only
def _status_table(self, bot: Bot, update: Update) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
@ -183,36 +150,15 @@ def _status_table(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active order`', bot=bot)
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
try:
df_statuses = self._rpc_status_table()
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message)
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _daily(bot: Bot, update: Update) -> None:
@authorized_only
def _daily(self, bot: Bot, update: Update) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
@ -220,58 +166,32 @@ def _daily(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
today = datetime.utcnow().date()
profit_days = {}
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config['fiat_display_currency']
try:
timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError):
timescale = 7
if not (isinstance(timescale, int) and timescale > 0):
send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot)
return
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = format(curdayprofit, '.8f')
stats = [
[
key,
'{value:.8f} {symbol}'.format(value=float(value), symbol=_CONF['stake_currency']),
'{value:.3f} {symbol}'.format(
value=_FIAT_CONVERT.convert_amount(
value,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
),
symbol=_CONF['fiat_display_currency']
try:
stats = self._rpc_daily_profit(
timescale,
stake_cur,
fiat_disp_cur
)
]
for key, value in profit_days.items()
]
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency'])
f'Profit {stake_cur}',
f'Profit {fiat_disp_cur}'
],
tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
@authorized_only
def _profit(self, bot: Bot, update: Update) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
@ -279,121 +199,65 @@ def _profit(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.first()
if not best_pair:
send_msg('*Status:* `no closed trade`', bot=bot)
return
bp_pair, bp_rate = best_pair
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_fiat = _FIAT_CONVERT.convert_amount(
profit_closed_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = _FIAT_CONVERT.convert_amount(
profit_all_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config['fiat_display_currency']
try:
stats = self._rpc_trade_statistics(
stake_cur,
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
markdown_msg = """
*ROI:* Close trades
`{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
`{profit_closed_fiat:.3f} {fiat}`
*ROI:* All trades
`{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
`{profit_all_fiat:.3f} {fiat}`
markdown_msg = "*ROI:* Close trades\n" \
f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
f"({profit_closed_percent:.2f}%)`\n" \
f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
f"*ROI:* All trades\n" \
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
f"*Total Trade Count:* `{trade_count}`\n" \
f"*First Trade opened:* `{first_trade_date}`\n" \
f"*Latest Trade opened:* `{latest_trade_date}`\n" \
f"*Avg. Duration:* `{avg_duration}`\n" \
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
self._send_msg(markdown_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
*Total Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}`
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format(
coin=_CONF['stake_currency'],
fiat=_CONF['fiat_display_currency'],
profit_closed_coin=profit_closed_coin,
profit_closed_percent=profit_closed_percent,
profit_closed_fiat=profit_closed_fiat,
profit_all_coin=profit_all_coin,
profit_all_percent=profit_all_percent,
profit_all_fiat=profit_all_fiat,
trade_count=len(trades),
first_trade_date=arrow.get(trades[0].open_date).humanize(),
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair,
best_rate=round(bp_rate * 100, 2),
)
send_msg(markdown_msg, bot=bot)
@authorized_only
def _balance(bot: Bot, update: Update) -> None:
"""
Handler for /balance
Returns current account balance per crypto
"""
@authorized_only
def _balance(self, bot: Bot, update: Update) -> None:
""" Handler for /balance """
try:
currencys, total, symbol, value = \
self._rpc_balance(self._config['fiat_display_currency'])
output = ''
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
output = '`All balances are zero.`'
for currency in currencys:
output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
for currency in balances:
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self._send_msg(output, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
""".format(**currency)
send_msg(output)
@authorized_only
def _start(bot: Bot, update: Update) -> None:
@authorized_only
def _start(self, bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
@ -401,14 +265,11 @@ def _start(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('*Status:* `already running`', bot=bot)
else:
update_state(State.RUNNING)
msg = self._rpc_start()
self._send_msg(msg, bot=bot)
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
@authorized_only
def _stop(self, bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
@ -416,15 +277,23 @@ def _stop(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('`Stopping trader ...`', bot=bot)
update_state(State.STOPPED)
else:
send_msg('*Status:* `already stopped`', bot=bot)
msg = self._rpc_stop()
self._send_msg(msg, bot=bot)
@authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None:
"""
Handler for /reload_conf.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_reload_conf()
self._send_msg(msg, bot=bot)
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
@authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
@ -432,32 +301,15 @@ def _forcesell(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trade_id = update.message.text.replace('/forcesell', '').strip()
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return
try:
self._rpc_forcesell(trade_id)
except RPCException as e:
self._send_msg(str(e), bot=bot)
# Query for trade
trade = Trade.query.filter(and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
send_msg('Invalid argument. See `/help` to view usage')
logger.warning('/forcesell: Invalid argument received')
return
_exec_forcesell(trade)
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
@authorized_only
def _performance(self, bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
@ -465,31 +317,21 @@ def _performance(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.all()
try:
trades = self._rpc_performance()
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=pair,
profit=round(rate * 100, 2),
count=count
) for i, (pair, rate, count) in enumerate(pair_rates))
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _count(bot: Bot, update: Update) -> None:
@authorized_only
def _count(self, bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
@ -497,23 +339,21 @@ def _count(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
try:
trades = self._rpc_count()
message = tabulate({
'current': [len(trades)],
'max': [_CONF['max_open_trades']]
}, headers=['current', 'max'], tablefmt='simple')
'max': [self._config['max_open_trades']],
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _help(bot: Bot, update: Update) -> None:
@authorized_only
def _help(self, bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
@ -521,25 +361,25 @@ def _help(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
message = """
*/start:* `Starts the trader`
*/stop:* `Stops the trader`
*/status [table]:* `Lists all open trades`
*table :* `will display trades in a table`
*/profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair`
*/daily <n>:* `Shows profit or loss per day, over the last n days`
*/count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency`
*/help:* `This help message`
*/version:* `Show version`
"""
send_msg(message, bot=bot)
message = "*/start:* `Starts the trader`\n" \
"*/stop:* `Stops the trader`\n" \
"*/status [table]:* `Lists all open trades`\n" \
" *table :* `will display trades in a table`\n" \
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
"regardless of profit`\n" \
"*/performance:* `Show performance of each finished trade grouped by pair`\n" \
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n" \
"*/count:* `Show number of trades running compared to allowed number of trades`" \
"\n" \
"*/balance:* `Show account balance per currency`\n" \
"*/help:* `This help message`\n" \
"*/version:* `Show version`"
self._send_msg(message, bot=bot)
@authorized_only
def _version(bot: Bot, update: Update) -> None:
@authorized_only
def _version(self, bot: Bot, update: Update) -> None:
"""
Handler for /version.
Show version information
@ -547,44 +387,10 @@ def _version(bot: Bot, update: Update) -> None:
:param update: message update
:return: None
"""
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
def _send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
@ -592,10 +398,7 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
:param parse_mode: telegram parse mode
:return: None
"""
if not is_enabled():
return
bot = bot or _UPDATER.bot
bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
@ -606,19 +409,26 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
try:
try:
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
logger.warning(
'Got Telegram NetworkError: %s! Trying one more time.',
'Telegram NetworkError: %s! Trying one more time.',
network_err.message
)
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
self._config['telegram']['chat_id'],
text=msg,
parse_mode=parse_mode,
reply_markup=reply_markup
)
except TelegramError as telegram_err:
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
logger.warning(
'TelegramError: %s! Giving up on that message.',
telegram_err.message
)

15
freqtrade/state.py Executable file
View File

@ -0,0 +1,15 @@
# pragma pylint: disable=too-few-public-methods
"""
Bot state constant
"""
import enum
class State(enum.Enum):
"""
Bot application states
"""
RUNNING = 0
STOPPED = 1
RELOAD_CONF = 2

32
freqtrade/strategy/__init__.py Executable file
View File

@ -0,0 +1,32 @@
import logging
from copy import deepcopy
from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__)
def import_strategy(strategy: IStrategy) -> IStrategy:
"""
Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it
"""
# Copy all attributes from base class and class
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
# Adjust module name
attr['__module__'] = 'freqtrade.strategy'
name = strategy.__class__.__name__
clazz = type(name, (IStrategy,), attr)
logger.debug(
'Imported strategy %s.%s as %s.%s',
strategy.__module__, strategy.__class__.__name__,
clazz.__module__, strategy.__class__.__name__,
)
# Modify global scope to declare class
globals()[name] = clazz
return clazz()

View File

@ -0,0 +1,240 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.indicator_helpers import fishers_inverse
from freqtrade.strategy.interface import IStrategy
class DefaultStrategy(IStrategy):
"""
Default Strategy provided by freqtrade bot.
You can override it with your own strategy
"""
# Minimal ROI designed for the strategy
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '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)
dataframe['fisher_rsi'] = fishers_inverse(dataframe['rsi'])
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
"""
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Previous Bollinger bands
# Because ta.BBANDS implementation is broken with small numbers, it actually
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
# and use middle band instead.
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
),
'sell'] = 1
return dataframe

48
freqtrade/strategy/interface.py Executable file
View File

@ -0,0 +1,48 @@
"""
IStrategy interface
This module defines the interface to apply for strategies
"""
from abc import ABC, abstractmethod
from typing import Dict
from pandas import DataFrame
class IStrategy(ABC):
"""
Interface for freqtrade strategies
Defines the mandatory structure must follow any custom strategies
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy
"""
minimal_roi: Dict
stoploss: float
ticker_interval: str
@abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:return: a Dataframe with all mandatory indicators for the strategies
"""
@abstractmethod
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
@abstractmethod
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with sell column
"""

134
freqtrade/strategy/resolver.py Executable file
View File

@ -0,0 +1,134 @@
# pragma pylint: disable=attribute-defined-outside-init
"""
This module load custom strategies
"""
import importlib.util
import inspect
import logging
import os
from collections import OrderedDict
from typing import Dict, Optional, Type
from freqtrade import constants
from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__)
class StrategyResolver(object):
"""
This class contains all the logic to load custom strategy class
"""
__slots__ = ['strategy']
def __init__(self, config: Optional[Dict] = None) -> None:
"""
Load the custom class from config parameter
:param config: configuration dictionary or None
"""
config = config or {}
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
self.strategy: IStrategy = self._load_strategy(strategy_name,
extra_dir=config.get('strategy_path'))
# Set attributes
# Check if we need to override configuration
if 'minimal_roi' in config:
self.strategy.minimal_roi = config['minimal_roi']
logger.info("Override strategy \'minimal_roi\' with value in config file.")
if 'stoploss' in config:
self.strategy.stoploss = config['stoploss']
logger.info(
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
)
if 'ticker_interval' in config:
self.strategy.ticker_interval = config['ticker_interval']
logger.info(
"Override strategy \'ticker_interval\' with value in config file: %s.",
config['ticker_interval']
)
# Sort and apply type conversions
self.strategy.minimal_roi = OrderedDict(sorted(
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
"""
Search and loads the specified strategy.
:param strategy_name: name of the module to import
:param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None
"""
current_path = os.path.dirname(os.path.realpath(__file__))
abs_paths = [
os.path.join(os.getcwd(), 'user_data', 'strategies'),
current_path,
]
if extra_dir:
# Add extra strategy directory on top of search paths
abs_paths.insert(0, extra_dir)
for path in abs_paths:
try:
strategy = self._search_strategy(path, strategy_name)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy)
except FileNotFoundError:
logger.warning('Path "%s" does not exist', path)
raise ImportError(
"Impossible to load Strategy '{}'. This class does not exist"
" or contains Python code errors".format(strategy_name)
)
@staticmethod
def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]:
"""
Returns a list of all possible strategies for the given module_path
:param module_path: absolute path to the module
:param strategy_name: Class name of the strategy
:return: Tuple with (name, class) or None
"""
# Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('unknown', module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
valid_strategies_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass)
if strategy_name == name and IStrategy in obj.__bases__
)
return next(valid_strategies_gen, None)
@staticmethod
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]:
"""
Search for the strategy_name in the given directory
:param directory: relative or absolute directory path
:return: name of the strategy class
"""
logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory)
for entry in os.listdir(directory):
# Only consider python files
if not entry.endswith('.py'):
logger.debug('Ignoring %s', entry)
continue
strategy = StrategyResolver._get_valid_strategies(
os.path.abspath(os.path.join(directory, entry)), strategy_name
)
if strategy:
return strategy()
return None

0
freqtrade/tests/__init__.py Normal file → Executable file
View File

662
freqtrade/tests/conftest.py Normal file → Executable file
View File

@ -1,5 +1,9 @@
# pragma pylint: disable=missing-docstring
import json
import logging
from datetime import datetime
from functools import reduce
from typing import Dict, Optional
from unittest.mock import MagicMock
import arrow
@ -7,10 +11,79 @@ import pytest
from jsonschema import validate
from telegram import Chat, Message, Update
from freqtrade.misc import CONF_SCHEMA
from freqtrade import constants
from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
logging.getLogger('').setLevel(logging.INFO)
@pytest.fixture(scope="module")
def log_has(line, logs):
# caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar')
# and we want to match line against foobar in the tuple
return reduce(lambda a, b: a or b,
filter(lambda x: x[2] == line, logs),
False)
def patch_exchange(mocker, api_mock=None) -> None:
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
patch_exchange(mocker, api_mock)
exchange = Exchange(config)
return exchange
# Functions for recurrent object patching
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patch _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: None
"""
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
patch_exchange(mocker, None)
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
return FreqtradeBot(config)
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
tickermock = MagicMock(return_value={'price_usd': 12345.0})
listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC',
'website_slug': 'bitcoin'},
{'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH',
'website_slug': 'ethereum'}
]})
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=tickermock,
listings=listmock,
)
@pytest.fixture(scope="function")
def default_conf():
""" Returns validated configuration suitable for most tests """
configuration = {
@ -18,6 +91,7 @@ def default_conf():
"stake_currency": "BTC",
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"ticker_interval": '5m',
"dry_run": True,
"minimal_roi": {
"40": 0.0,
@ -26,7 +100,10 @@ def default_conf():
"0": 0.04
},
"stoploss": -0.10,
"unfilledtimeout": 600,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": {
"ask_last_balance": 0.0
},
@ -36,11 +113,10 @@ def default_conf():
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_BCC"
"ETH/BTC",
"LTC/BTC",
"XRP/BTC",
"NEO/BTC"
]
},
"telegram": {
@ -48,9 +124,11 @@ def default_conf():
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
"initial_state": "running",
"db_url": "sqlite://",
"loglevel": logging.DEBUG,
}
validate(configuration, CONF_SCHEMA)
validate(configuration, constants.CONF_SCHEMA)
return configuration
@ -61,6 +139,11 @@ def update():
return _update
@pytest.fixture
def fee():
return MagicMock(return_value=0.0025)
@pytest.fixture
def ticker():
return MagicMock(return_value={
@ -89,46 +172,178 @@ def ticker_sell_down():
@pytest.fixture
def health():
return MagicMock(return_value=[{
'Currency': 'BTC',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'ETH',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'TRST',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'SWT',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'BCC',
'IsActive': False,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}])
def markets():
return MagicMock(return_value=[
{
'id': 'ethbtc',
'symbol': 'ETH/BTC',
'base': 'ETH',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'tknbtc',
'symbol': 'TKN/BTC',
'base': 'TKN',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'blkbtc',
'symbol': 'BLK/BTC',
'base': 'BLK',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'ltcbtc',
'symbol': 'LTC/BTC',
'base': 'LTC',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'xrpbtc',
'symbol': 'XRP/BTC',
'base': 'XRP',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'neobtc',
'symbol': 'NEO/BTC',
'base': 'NEO',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
}
])
@pytest.fixture
def markets_empty():
return MagicMock(return_value=[])
@pytest.fixture(scope='function')
def limit_buy_order():
return {
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@ -136,12 +351,14 @@ def limit_buy_order():
def limit_buy_order_old():
return {
'id': 'mocked_limit_buy_old',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@ -149,12 +366,14 @@ def limit_buy_order_old():
def limit_sell_order_old():
return {
'id': 'mocked_limit_sell_old',
'type': 'LIMIT_SELL',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@ -162,12 +381,14 @@ def limit_sell_order_old():
def limit_buy_order_old_partial():
return {
'id': 'mocked_limit_buy_old_partial',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 67.99181073,
'status': 'open'
}
@ -175,44 +396,317 @@ def limit_buy_order_old_partial():
def limit_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'type': 'limit',
'side': 'sell',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001173,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@pytest.fixture
def ticker_history():
return [
{
"O": 8.794e-05,
"H": 8.948e-05,
"L": 8.794e-05,
"C": 8.88e-05,
"V": 991.09056638,
"T": "2017-11-26T08:50:00",
"BV": 0.0877869
},
{
"O": 8.88e-05,
"H": 8.942e-05,
"L": 8.88e-05,
"C": 8.893e-05,
"V": 658.77935965,
"T": "2017-11-26T08:55:00",
"BV": 0.05874751
},
{
"O": 8.891e-05,
"H": 8.893e-05,
"L": 8.875e-05,
"C": 8.877e-05,
"V": 7920.73570705,
"T": "2017-11-26T09:00:00",
"BV": 0.7039405
}
[
1511686200000, # unix timestamp ms
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
0.0877869, # volume (in quote currency)
],
[
1511686500000,
8.88e-05,
8.942e-05,
8.88e-05,
8.893e-05,
0.05874751,
],
[
1511686800000,
8.891e-05,
8.893e-05,
8.875e-05,
8.877e-05,
0.7039405
]
]
@pytest.fixture
def tickers():
return MagicMock(return_value={
'ETH/BTC': {
'symbol': 'ETH/BTC',
'timestamp': 1522014806207,
'datetime': '2018-03-25T21:53:26.207Z',
'high': 0.061697,
'low': 0.060531,
'bid': 0.061588,
'bidVolume': 3.321,
'ask': 0.061655,
'askVolume': 0.212,
'vwap': 0.06105296,
'open': 0.060809,
'close': 0.060761,
'first': None,
'last': 0.061588,
'change': 1.281,
'percentage': None,
'average': None,
'baseVolume': 111649.001,
'quoteVolume': 6816.50176926,
'info': {}
},
'TKN/BTC': {
'symbol': 'TKN/BTC',
'timestamp': 1522014806169,
'datetime': '2018-03-25T21:53:26.169Z',
'high': 0.01885,
'low': 0.018497,
'bid': 0.018799,
'bidVolume': 8.38,
'ask': 0.018802,
'askVolume': 15.0,
'vwap': 0.01869197,
'open': 0.018585,
'close': 0.018573,
'baseVolume': 81058.66,
'quoteVolume': 2247.48374509,
},
'BLK/BTC': {
'symbol': 'BLK/BTC',
'timestamp': 1522014806072,
'datetime': '2018-03-25T21:53:26.720Z',
'high': 0.007745,
'low': 0.007512,
'bid': 0.007729,
'bidVolume': 0.01,
'ask': 0.007743,
'askVolume': 21.37,
'vwap': 0.00761466,
'open': 0.007653,
'close': 0.007652,
'first': None,
'last': 0.007743,
'change': 1.176,
'percentage': None,
'average': None,
'baseVolume': 295152.26,
'quoteVolume': 1515.14631229,
'info': {}
},
'LTC/BTC': {
'symbol': 'LTC/BTC',
'timestamp': 1523787258992,
'datetime': '2018-04-15T10:14:19.992Z',
'high': 0.015978,
'low': 0.0157,
'bid': 0.015954,
'bidVolume': 12.83,
'ask': 0.015957,
'askVolume': 0.49,
'vwap': 0.01581636,
'open': 0.015823,
'close': 0.01582,
'first': None,
'last': 0.015951,
'change': 0.809,
'percentage': None,
'average': None,
'baseVolume': 88620.68,
'quoteVolume': 1401.65697943,
'info': {}
},
'ETH/USDT': {
'symbol': 'ETH/USDT',
'timestamp': 1522014804118,
'datetime': '2018-03-25T21:53:24.118Z',
'high': 530.88,
'low': 512.0,
'bid': 529.73,
'bidVolume': 0.2,
'ask': 530.21,
'askVolume': 0.2464,
'vwap': 521.02438405,
'open': 527.27,
'close': 528.42,
'first': None,
'last': 530.21,
'change': 0.558,
'percentage': None,
'average': None,
'baseVolume': 72300.0659,
'quoteVolume': 37670097.3022171,
'info': {}
},
'TKN/USDT': {
'symbol': 'TKN/USDT',
'timestamp': 1522014806198,
'datetime': '2018-03-25T21:53:26.198Z',
'high': 8718.0,
'low': 8365.77,
'bid': 8603.64,
'bidVolume': 0.15846,
'ask': 8603.67,
'askVolume': 0.069147,
'vwap': 8536.35621697,
'open': 8680.0,
'close': 8680.0,
'first': None,
'last': 8603.67,
'change': -0.879,
'percentage': None,
'average': None,
'baseVolume': 30414.604298,
'quoteVolume': 259629896.48584127,
'info': {}
},
'BLK/USDT': {
'symbol': 'BLK/USDT',
'timestamp': 1522014806145,
'datetime': '2018-03-25T21:53:26.145Z',
'high': 66.95,
'low': 63.38,
'bid': 66.473,
'bidVolume': 4.968,
'ask': 66.54,
'askVolume': 2.704,
'vwap': 65.0526901,
'open': 66.43,
'close': 66.383,
'first': None,
'last': 66.5,
'change': 0.105,
'percentage': None,
'average': None,
'baseVolume': 294106.204,
'quoteVolume': 19132399.743954,
'info': {}
},
'LTC/USDT': {
'symbol': 'LTC/USDT',
'timestamp': 1523787257812,
'datetime': '2018-04-15T10:14:18.812Z',
'high': 129.94,
'low': 124.0,
'bid': 129.28,
'bidVolume': 0.03201,
'ask': 129.52,
'askVolume': 0.14529,
'vwap': 126.92838682,
'open': 127.0,
'close': 127.1,
'first': None,
'last': 129.28,
'change': 1.795,
'percentage': None,
'average': None,
'baseVolume': 59698.79897,
'quoteVolume': 29132399.743954,
'info': {}
}
})
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file))
# FIX:
# Create an fixture/function
# that inserts a trade of some type and open-status
# return the open-order-id
# See tests in rpc/main that could use this
@pytest.fixture(scope="function")
def trades_for_order():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 8.0,
'fee': {'cost': 0.008, 'currency': 'LTC'}}]
@pytest.fixture(scope="function")
def trades_for_order2():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}},
{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
@pytest.fixture
def buy_order_fee():
return {
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441,
'amount': 8.0,
'remaining': 90.99181073,
'status': 'closed',
'fee': None
}

705
freqtrade/tests/exchange/test_exchange.py Normal file → Executable file
View File

@ -1,23 +1,39 @@
# pragma pylint: disable=missing-docstring,C0103
from unittest.mock import MagicMock
from requests.exceptions import RequestException
from random import randint
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
import logging
from copy import deepcopy
from datetime import datetime
from random import randint
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, cancel_order, get_name, get_fee
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import API_RETRY_COUNT, Exchange
from freqtrade.tests.conftest import get_patched_exchange, log_has
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
"""Function to test ccxt exception handling """
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):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(config=default_conf)
assert ('freqtrade.exchange',
logging.INFO,
'Instance is running with dry_run enabled'
) in caplog.record_tuples
caplog.set_level(logging.INFO)
get_patched_exchange(mocker, default_conf)
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
def test_init_exception(default_conf, mocker):
@ -26,186 +42,661 @@ def test_init_exception(default_conf, mocker):
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
init(config=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_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist'])
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
Exchange(default_conf)
def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(
return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
})
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
Exchange(conf)
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.get_markets = MagicMock(side_effect=RequestException())
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
assert ('freqtrade.exchange',
logging.WARNING,
'Unable to validate pairs (assuming they are correct). Reason: '
) in caplog.record_tuples
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
Exchange(default_conf)
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
Exchange(default_conf)
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
caplog.record_tuples)
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
api_mock = MagicMock()
api_mock.name = MagicMock(return_value='binance')
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
with pytest.raises(
OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
):
Exchange(conf)
def test_exchangehas(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):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_buy_' in order['id']
def test_buy_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.buy = MagicMock(
return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_buy_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_sell_' in order['id']
def test_sell_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.sell = MagicMock(
return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_sell_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 999.9
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balance(currency='BTC') == 999.9
def test_get_balance_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_balance = MagicMock(return_value=123.4)
mocker.patch('freqtrade.exchange._API', api_mock)
api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 123.4
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_balance(currency='BTC') == 123.4
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
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):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balances() == []
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balances() == {}
def test_get_balances_prod(default_conf, mocker):
balance_item = {
'Currency': '1ST',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': None
'free': 10.0,
'total': 10.0,
'used': 0.0
}
api_mock = MagicMock()
api_mock.get_balances = MagicMock(
return_value=[balance_item, balance_item, balance_item])
mocker.patch('freqtrade.exchange._API', api_mock)
api_mock.fetch_balance = MagicMock(return_value={
'1ST': balance_item,
'2ST': balance_item,
'3ST': balance_item
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert len(exchange.get_balances()) == 3
assert exchange.get_balances()['1ST']['free'] == 10.0
assert exchange.get_balances()['1ST']['total'] == 10.0
assert exchange.get_balances()['1ST']['used'] == 0.0
assert len(get_balances()) == 3
assert get_balances()[0]['Currency'] == '1ST'
assert get_balances()[0]['Balance'] == 10.0
assert get_balances()[0]['Available'] == 10.0
assert get_balances()[0]['Pending'] == 0.0
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_balances", "fetch_balance")
def test_get_ticker(mocker, ticker):
def test_get_tickers(default_conf, mocker):
api_mock = MagicMock()
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
api_mock.get_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {'ETH/BTC': {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}, 'BCH/BTC': {
'symbol': 'BCH/BTC',
'bid': 0.6,
'ask': 0.5,
'last': 41,
}
}
api_mock.fetch_tickers = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticker = get_ticker(pair='BTC_ETH')
tickers = exchange.get_tickers()
assert 'ETH/BTC' in tickers
assert 'BCH/BTC' in tickers
assert tickers['ETH/BTC']['bid'] == 0.5
assert tickers['ETH/BTC']['ask'] == 1
assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_tickers", "fetch_tickers")
with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
api_mock.fetch_tickers = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
def test_get_ticker(default_conf, mocker):
api_mock = MagicMock()
tick = {
'symbol': 'ETH/BTC',
'bid': 0.00001098,
'ask': 0.00001099,
'last': 0.0001,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticker = exchange.get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# change the ticker
tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}}
api_mock.get_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# if not caching the result we should get the same ticker
ticker = get_ticker(pair='BTC_ETH', refresh=False)
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# if not fetching a new result we should get the cached ticker
ticker = exchange.get_ticker(pair='ETH/BTC')
# force ticker refresh
ticker = get_ticker(pair='BTC_ETH', refresh=True)
assert api_mock.fetch_ticker.call_count == 1
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1
assert 'ETH/BTC' in exchange._cached_ticker
assert exchange._cached_ticker['ETH/BTC']['bid'] == 0.5
assert exchange._cached_ticker['ETH/BTC']['ask'] == 1
# Test caching
api_mock.fetch_ticker = MagicMock()
exchange.get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_ticker", "fetch_ticker",
pair='ETH/BTC', refresh=True)
api_mock.fetch_ticker = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
if since:
assert since > data[-1][0]
return []
return data
return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker):
api_mock = MagicMock()
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1
assert ticks[0][2] == 2
assert ticks[0][3] == 3
assert ticks[0][4] == 4
assert ticks[0][5] == 5
# change ticker and ensure tick changes
new_tick = [
[
1511686210000, # unix timestamp ms
6, # open
7, # high
8, # low
9, # close
10, # volume (in quote currency)
]
]
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6
assert ticks[0][2] == 7
assert ticks[0][3] == 8
assert ticks[0][4] == 9
assert ticks[0][5] == 10
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_ticker_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_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker):
api_mock = MagicMock()
# GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last)
tick = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651
assert ticks[0][3] == 0.07649
assert ticks[0][4] == 0.07651
assert ticks[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000
assert ticks[9][1] == 0.07666
assert ticks[9][2] == 0.07671
assert ticks[9][3] == 0.07666
assert ticks[9][4] == 0.07668
assert ticks[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last)
tick = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766
assert ticks[0][3] == 0.07627
assert ticks[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000
assert ticks[9][1] == 0.07671
assert ticks[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783
def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert cancel_order(order_id='123') is None
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
# Ensure that if not dry_run, we should call API
def test_cancel_order(default_conf, mocker):
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
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
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"cancel_order", "cancel_order",
order_id='_', pair='TKN/BTC')
def test_get_order(default_conf, mocker):
default_conf['dry_run'] = True
order = MagicMock()
order.myid = 123
exchange = get_patched_exchange(mocker, default_conf)
exchange._dry_run_open_orders['X'] = order
print(exchange.get_order('X', 'TKN/BTC'))
assert exchange.get_order('X', 'TKN/BTC').myid == 123
default_conf['dry_run'] = False
api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value=456)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
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
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_order', 'fetch_order',
order_id='_', pair='TKN/BTC')
def test_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'bittrex'
init(default_conf)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
assert get_name() == 'Bittrex'
assert exchange.name == 'Binance'
def test_id(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
assert exchange.id == 'binance'
def test_get_pair_detail_url(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
default_conf['exchange']['name'] = 'bittrex'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
default_conf['exchange']['name'] = 'poloniex'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert '' == url
assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples)
def test_get_trades_for_order(default_conf, mocker):
order_id = 'ABCD-ABCD'
since = datetime(2018, 5, 5)
default_conf["dry_run"] = False
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
api_mock = MagicMock()
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
'order': 'ABCD-ABCD',
'info': {'pair': 'XLTCZBTC',
'time': 1519860024.4388,
'type': 'buy',
'ordertype': 'limit',
'price': '20.00000',
'cost': '38.62000',
'fee': '0.06179',
'vol': '5',
'id': 'ABCD-ABCD'},
'timestamp': 1519860024438,
'datetime': '2018-02-28T23:20:24.438Z',
'symbol': 'LTC/BTC',
'type': 'limit',
'side': 'buy',
'price': 165.0,
'amount': 0.2340606,
'fee': {'cost': 0.06179, 'currency': 'BTC'}
}])
exchange = get_patched_exchange(mocker, default_conf, api_mock)
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert len(orders) == 1
assert orders[0]['price'] == 165
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_trades_for_order', 'fetch_my_trades',
order_id=order_id, pair='LTC/BTC', since=since)
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
def test_get_markets(default_conf, mocker, markets):
api_mock = MagicMock()
api_mock.fetch_markets = markets
exchange = get_patched_exchange(mocker, default_conf, api_mock)
ret = exchange.get_markets()
assert isinstance(ret, list)
assert len(ret) == 6
assert ret[0]["id"] == "ethbtc"
assert ret[0]["symbol"] == "ETH/BTC"
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_markets', 'fetch_markets')
def test_get_fee(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(default_conf)
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(return_value={
'type': 'taker',
'currency': 'BTC',
'rate': 0.025,
'cost': 0.05
})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert get_fee() == 0.0025
assert exchange.get_fee() == 0.025
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_fee', 'calculate_fee')
def test_get_amount_lots(default_conf, mocker):
api_mock = MagicMock()
api_mock.amount_to_lots = MagicMock(return_value=1.0)
api_mock.markets = None
marketmock = MagicMock()
api_mock.load_markets = marketmock
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
assert marketmock.call_count == 1

View File

@ -1,337 +0,0 @@
# pragma pylint: disable=missing-docstring,C0103
import pytest
from unittest.mock import MagicMock
from requests.exceptions import ContentDecodingError
from freqtrade.exchange.bittrex import Bittrex
import freqtrade.exchange.bittrex as btx
# Eat this flake8
# +------------------+
# | bittrex.Bittrex |
# +------------------+
# |
# (mock Fake_bittrex)
# |
# +-----------------------------+
# | freqtrade.exchange.Bittrex |
# +-----------------------------+
# Call into Bittrex will flow up to the
# external package bittrex.Bittrex.
# By inserting a mock, we redirect those
# calls.
# The faked bittrex API is called just 'fb'
# The freqtrade.exchange.Bittrex is a
# wrapper, and is called 'wb'
def _stub_config():
return {'key': '',
'secret': ''}
class FakeBittrex():
def __init__(self, success=True):
self.success = True # Believe in yourself
self.result = None
self.get_ticker_call_count = 0
# This is really ugly, doing side-effect during instance creation
# But we're allowed to in testing-code
btx._API = MagicMock()
btx._API.buy_limit = self.fake_buysell_limit
btx._API.sell_limit = self.fake_buysell_limit
btx._API.get_balance = self.fake_get_balance
btx._API.get_balances = self.fake_get_balances
btx._API.get_ticker = self.fake_get_ticker
btx._API.get_order = self.fake_get_order
btx._API.cancel = self.fake_cancel_order
btx._API.get_markets = self.fake_get_markets
btx._API.get_market_summaries = self.fake_get_market_summaries
btx._API_V2 = MagicMock()
btx._API_V2.get_candles = self.fake_get_candles
btx._API_V2.get_wallet_health = self.fake_get_wallet_health
def fake_buysell_limit(self, pair, amount, limit):
return {'success': self.success,
'result': {'uuid': '1234'},
'message': 'barter'}
def fake_get_balance(self, cur):
return {'success': self.success,
'result': {'Balance': 1234},
'message': 'unbalanced'}
def fake_get_balances(self):
return {'success': self.success,
'result': [{'BTC_ETH': 1234}],
'message': 'no balances'}
def fake_get_ticker(self, pair):
self.get_ticker_call_count += 1
return self.result or {'success': self.success,
'result': {'Bid': 1, 'Ask': 1, 'Last': 1},
'message': 'NO_API_RESPONSE'}
def fake_get_candles(self, pair, interval):
return self.result or {'success': self.success,
'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],
'message': 'candles lit'}
def fake_get_order(self, uuid):
return {'success': self.success,
'result': {'OrderUuid': 'ABC123',
'Type': 'Type',
'Exchange': 'BTC_ETH',
'Opened': True,
'PricePerUnit': 1,
'Quantity': 1,
'QuantityRemaining': 1,
'Closed': True
},
'message': 'lost'}
def fake_cancel_order(self, uuid):
return self.result or {'success': self.success,
'message': 'no such order'}
def fake_get_markets(self):
return self.result or {'success': self.success,
'message': 'market gone',
'result': [{'MarketName': '-_'}]}
def fake_get_market_summaries(self):
return self.result or {'success': self.success,
'message': 'no summary',
'result': ['sum']}
def fake_get_wallet_health(self):
return self.result or {'success': self.success,
'message': 'bad health',
'result': [{'Health': {'Currency': 'BTC_ETH',
'IsActive': True,
'LastChecked': 0},
'Currency': {'Notice': True}}]}
# The freqtrade.exchange.bittrex is called wrap_bittrex
# to not confuse naming with bittrex.bittrex
def make_wrap_bittrex():
conf = _stub_config()
wb = btx.Bittrex(conf)
return wb
def test_exchange_bittrex_class():
conf = _stub_config()
b = Bittrex(conf)
assert isinstance(b, Bittrex)
slots = dir(b)
for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
'get_ticker', 'get_ticker_history', 'get_order',
'cancel_order', 'get_pair_detail_url', 'get_markets',
'get_market_summaries', 'get_wallet_health']:
assert name in slots
# FIX: ensure that the slot is also a method in the class
# getattr(b, name) => bound method Bittrex.buy
# type(getattr(b, name)) => class 'method'
def test_exchange_bittrex_fee():
fee = Bittrex.fee.__get__(Bittrex)
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
def test_exchange_bittrex_buy_good(mocker):
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.buy('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
wb.buy('BAD', 1, 1)
def test_exchange_bittrex_sell_good(mocker):
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.sell('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
uuid = wb.sell('BAD', 1, 1)
def test_exchange_bittrex_get_balance(mocker):
wb = make_wrap_bittrex()
fb = FakeBittrex()
bal = wb.get_balance('BTC_ETH')
assert bal == fb.fake_get_balance(1)['result']['Balance']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'unbalanced'):
wb.get_balance('BTC_ETH')
def test_exchange_bittrex_get_balances():
wb = make_wrap_bittrex()
fb = FakeBittrex()
bals = wb.get_balances()
assert bals == fb.fake_get_balances()['result']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'no balances'):
wb.get_balances()
def test_exchange_bittrex_get_ticker():
wb = make_wrap_bittrex()
fb = FakeBittrex()
# Poll ticker, which updates the cache
tick = wb.get_ticker('BTC_ETH')
for x in ['bid', 'ask', 'last']:
assert x in tick
# Ensure the side-effect was made (update the ticker cache)
assert 'BTC_ETH' in wb.cached_ticker.keys()
# taint the cache, so we can recognize the cache wall utilized
wb.cached_ticker['BTC_ETH']['bid'] = 1234
# Poll again, getting the cached result
fb.get_ticker_call_count = 0
tick = wb.get_ticker('BTC_ETH', False)
# Ensure the result was from the cache, and that we didn't call exchange
assert wb.cached_ticker['BTC_ETH']['bid'] == 1234
assert fb.get_ticker_call_count == 0
def test_exchange_bittrex_get_ticker_bad():
wb = make_wrap_bittrex()
fb = FakeBittrex()
fb.result = {'success': True,
'result': {'Bid': 1}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex params.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': False,
'message': 'gone bad'
}
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
wb.get_ticker('BTC_ETH')
def test_exchange_bittrex_get_ticker_history_one():
wb = make_wrap_bittrex()
FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 1)
def test_exchange_bittrex_get_ticker_history():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ValueError, match=r'.*Cannot parse tick_interval.*'):
wb.get_ticker_history('BTC_ETH', 2)
fb.success = False
with pytest.raises(btx.OperationalException, match=r'candles lit.*'):
wb.get_ticker_history('BTC_ETH', 5)
fb.success = True
with pytest.raises(ContentDecodingError, match=r'.*Got invalid response from bittrex.*'):
fb.result = {'bad': 0}
wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'):
fb.result = {'success': True,
'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing
'message': 'candles lit'}
wb.get_ticker_history('BTC_ETH', 5)
def test_exchange_bittrex_get_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
order = wb.get_order('someUUID')
assert order['id'] == 'ABC123'
fb.success = False
with pytest.raises(btx.OperationalException, match=r'lost'):
wb.get_order('someUUID')
def test_exchange_bittrex_cancel_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'no such order'):
fb.success = False
wb.cancel_order('someUUID')
# Note: this can be a bug in exchange.bittrex._validate_response
with pytest.raises(KeyError):
fb.result = {'success': False} # message is missing!
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'foo'):
fb.result = {'success': False, 'message': 'foo'}
wb.cancel_order('someUUID')
def test_exchange_get_pair_detail_url():
wb = make_wrap_bittrex()
assert wb.get_pair_detail_url('BTC_ETH')
def test_exchange_get_markets():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_markets()
assert x == ['__']
with pytest.raises(btx.OperationalException, match=r'market gone'):
fb.success = False
wb.get_markets()
def test_exchange_get_market_summaries():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert ['sum'] == wb.get_market_summaries()
with pytest.raises(btx.OperationalException, match=r'no summary'):
fb.success = False
wb.get_market_summaries()
def test_exchange_get_wallet_health():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_wallet_health()
assert x[0]['Currency'] == 'BTC_ETH'
with pytest.raises(btx.OperationalException, match=r'bad health'):
fb.success = False
wb.get_wallet_health()
def test_validate_response_success():
response = {
'message': '',
'result': [],
}
Bittrex._validate_response(response)
def test_validate_response_no_api_response():
response = {
'message': 'NO_API_RESPONSE',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
Bittrex._validate_response(response)
def test_validate_response_min_trade_requirement_not_met():
response = {
'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
Bittrex._validate_response(response)

807
freqtrade/tests/optimize/test_backtesting.py Normal file → Executable file
View File

@ -1,140 +1,564 @@
# pragma pylint: disable=missing-docstring,W0212
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import logging
import json
import math
import pandas as pd
import random
from copy import deepcopy
from typing import List
from unittest.mock import MagicMock
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize import preprocess
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
import freqtrade.optimize.backtesting as backtesting
import numpy as np
import pandas as pd
import pytest
from arrow import Arrow
from freqtrade import DependencyException, constants, optimize
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
start)
from freqtrade.tests.conftest import log_has, patch_exchange
def test_generate_text_table():
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5))
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
'BTC_ETH 2 15.00 0.60000000 100.0 2 0\n' # noqa
'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa
def get_args(args) -> List[str]:
return Arguments(args, '').get_parsed_arg()
def test_get_timeframe():
data = preprocess(optimize.load_data(
None, ticker_interval=1, pairs=['BTC_UNITEST']))
min_date, max_date = get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_backtest(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 10, True)
assert not results.empty
def test_backtest_1min_ticker_interval(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 1, True)
assert not results.empty
def trim_dictlist(dl, num):
def trim_dictlist(dict_list, num):
new = {}
for pair, pair_data in dl.items():
for pair, pair_data in dict_list.items():
new[pair] = pair_data[num:]
return new
def load_data_test(what):
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -100)
pair = data['BTC_UNITEST']
timerange = TimeRange(None, 'line', 0, -101)
data = optimize.load_data(None, ticker_interval='1m',
pairs=['UNITTEST/BTC'], timerange=timerange)
pair = data['UNITTEST/BTC']
datalen = len(pair)
# Depending on the what parameter we now adjust the
# loaded data looks:
# pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123,
# 'C': 0.123, 'V': 123.123,
# 'T': '2017-11-04T23:02:00', 'BV': 0.123}]
# pair :: [[ 1509836520000, unix timestamp in ms
# 0.00162008, open
# 0.00162008, high
# 0.00162008, low
# 0.00162008, close
# 108.14853839 base volume
# ]]
base = 0.001
if what == 'raise':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': x * base, # But replace O,H,L,C
'H': x * base + 0.0001,
'L': x * base - 0.0001,
'C': x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
x * base, # But replace O,H,L,C
x * base + 0.0001,
x * base - 0.0001,
x * base,
pair[x][5], # Keep old volume
] for x in range(0, datalen)
]}
if what == 'lower':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': 1 - x * base, # But replace O,H,L,C
'H': 1 - x * base + 0.0001,
'L': 1 - x * base - 0.0001,
'C': 1 - x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
1 - x * base, # But replace O,H,L,C
1 - x * base + 0.0001,
1 - x * base - 0.0001,
1 - x * base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
if what == 'sine':
hz = 0.1 # frequency
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
# But replace O,H,L,C
'O': math.sin(x * hz) / 1000 + base,
'H': math.sin(x * hz) / 1000 + base + 0.0001,
'L': math.sin(x * hz) / 1000 + base - 0.0001,
'C': math.sin(x * hz) / 1000 + base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001,
math.sin(x * hz) / 1000 + base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
return data
def simple_backtest(config, contour, num_results):
def simple_backtest(config, contour, num_results, mocker) -> None:
patch_exchange(mocker)
backtesting = Backtesting(config)
data = load_data_test(contour)
processed = optimize.preprocess(data)
processed = backtesting.tickerdata_to_dataframe(data)
assert isinstance(processed, dict)
results = backtest(config['stake_amount'], processed, 1, True)
results = backtesting.backtest(
{
'stake_amount': config['stake_amount'],
'processed': processed,
'max_open_trades': 1,
'realistic': True
}
)
# results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results
# Test backtest on offline data
# loaded by freqdata/optimize/__init__.py::load_data()
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
timerange=None, exchange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': tickerdata}
return pairdata
def test_backtest2(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 10, True)
# use for mock freqtrade.exchange.get_ticker_history'
def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -201)
return ticks[pair]
# FIX: fixturize this?
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
data = trim_dictlist(data, -201)
patch_exchange(mocker)
backtesting = Backtesting(conf)
return {
'stake_amount': conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True,
'record': record
}
def _trend(signals, buy_value, sell_value):
n = len(signals['low'])
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(signals['buy'])):
if random.random() > 0.5: # Both buy and sell signals at same timeframe
buy[i] = buy_value
sell[i] = sell_value
signals['buy'] = buy
signals['sell'] = sell
return signals
def _trend_alternate(dataframe=None):
signals = dataframe
low = signals['low']
n = len(low)
buy = np.zeros(n)
sell = np.zeros(n)
for i in range(0, len(buy)):
if i % 2 == 0:
buy[i] = 1
else:
sell[i] = 1
signals['buy'] = buy
signals['sell'] = sell
return dataframe
# Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo',
'--export-filename', 'foo_bar.json'
]
config = setup_configuration(get_args(args))
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs' in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' in config
assert log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
assert 'exportfilename' in config
assert log_has(
'Storing backtest results to {} ...'.format(config['exportfilename']),
caplog.record_tuples
)
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
setup_configuration(get_args(args))
def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
args = get_args(args)
start(args)
assert log_has(
'Starting freqtrade in Backtesting mode',
caplog.record_tuples
)
assert start_mock.call_count == 1
def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
"""
patch_exchange(mocker)
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend)
assert callable(backtesting.populate_sell_trend)
get_fee.assert_called()
assert backtesting.fee == 0.5
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist)
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
def test_get_timeframe(default_conf, mocker) -> None:
"""
Test Backtesting.get_timeframe() method
"""
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(
optimize.load_data(
None,
ticker_interval='1m',
pairs=['UNITTEST/BTC']
)
)
min_date, max_date = backtesting.get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
def test_generate_text_table(default_conf, mocker):
"""
Test Backtesting.generate_text_table() method
"""
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
results = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
result_str = (
'| pair | buy count | avg profit % | '
'total profit BTC | avg duration | profit | loss |\n'
'|:--------|------------:|---------------:|'
'-------------------:|---------------:|---------:|-------:|\n'
'| ETH/BTC | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |\n'
'| TOTAL | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |'
)
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = 1
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '-100'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
exists = [
'Using local backtesting data (using whitelist in given config) ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 '
'up to 2017-11-14T22:59:00+00:00 (0 days)..'
]
for line in exists:
assert log_has(line, caplog.record_tuples)
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method if no data is found
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = "1m"
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples)
def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
"""
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC'
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
data_processed = backtesting.tickerdata_to_dataframe(data)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': data_processed,
'max_open_trades': 10,
'realistic': True
}
)
assert not results.empty
assert len(results) == 2
expected = pd.DataFrame(
{'pair': [pair, pair],
'profit_percent': [0.00148826, 0.00075313],
'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, 23, 15, 0).datetime,
Arrow(2018, 1, 30, 4, 20, 0).datetime],
'open_index': [77, 183],
'close_index': [132, 193],
'trade_duration': [275, 50],
'open_at_end': [False, False],
'open_rate': [0.10432, 0.103364],
'close_rate': [0.104999, 0.10396]})
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
assert ln is not None
assert round(ln.iloc[0]["close"], 6) == round(t["open_rate"], 6)
# check close trade
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
assert round(ln.iloc[0]["close"], 6) == round(t["close_rate"], 6)
def test_processed(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
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)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 1,
'realistic': True
}
)
assert not results.empty
assert len(results) == 1
def test_processed(default_conf, mocker) -> None:
"""
Test Backtesting.backtest() method with offline data
"""
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = optimize.preprocess(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
@ -142,36 +566,175 @@ def test_processed(default_conf, mocker):
assert col in cols
def test_backtest_pricecontours(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
simple_backtest(default_conf, contour, numres, mocker)
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1)
pairdata = {'BTC_UNITEST': tickerdata}
return trim_dictlist(pairdata, -100)
# Test backtest using offline data (testdata directory)
def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
ticks = [1, 5]
fun = Backtesting(default_conf).populate_buy_trend
for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert not results.empty
def test_backtest_start(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
mocker.patch.multiple('freqtrade.optimize',
load_data=mocked_load_data)
def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 1
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 0
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4
# One trade was force-closed at the end
assert len(results.loc[results.open_at_end]) == 1
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
backtesting = Backtesting(default_conf)
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_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],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True]
})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end) in records:
assert pair == 'UNITTEST/BTC'
assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints
assert isinstance(date_buy, float)
assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix:
assert buy_index > oix
oix = buy_index
assert dur > 0
def test_backtest_start_live(default_conf, mocker, caplog):
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
new=lambda s, n, i: _load_pair_as_ticks(n, i))
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
args = MagicMock()
args.ticker_interval = 1
args.level = 10
args.live = False
args.live = True
args.datadir = None
backtesting.start(args)
args.export = None
args.strategy = 'DefaultStrategy'
args.timerange = '-100' # needed due to MagicMock malleability
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', 'freqtrade/tests/testdata',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100',
'--realistic-simulation'
]
args = get_args(args)
start(args)
# check the logs, that will contain the backtest result
exists = ['Using max_open_trades: 1 ...',
exists = [
'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 up to 2017-11-14T22:59:00+00:00 ...']
'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 --realistic-simulation detected ...'
]
for line in exists:
assert ('freqtrade.optimize.backtesting',
logging.INFO,
line) in caplog.record_tuples
assert log_has(line, caplog.record_tuples)

479
freqtrade/tests/optimize/test_hyperopt.py Normal file → Executable file
View File

@ -1,223 +1,348 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
log_results, save_trials, read_trials
import os
from copy import deepcopy
from unittest.mock import MagicMock
import pandas as pd
import pytest
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.optimize.hyperopt import Hyperopt, start
from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args
# Avoid to reinit the same object again and again
_HYPEROPT_INITIALIZED = False
_HYPEROPT = None
def test_loss_calculation_prefer_correct_trade_count():
correct = calculate_loss(1, TARGET_TRADES, 20)
over = calculate_loss(1, TARGET_TRADES + 100, 20)
under = calculate_loss(1, TARGET_TRADES - 100, 20)
assert over > correct
assert under > correct
@pytest.fixture(scope='function')
def init_hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT
if not _HYPEROPT_INITIALIZED:
patch_exchange(mocker)
_HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True
def test_loss_calculation_prefer_shorter_trades():
shorter = calculate_loss(1, 100, 20)
longer = calculate_loss(1, 100, 30)
assert shorter < longer
def test_loss_calculation_has_limited_profit():
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
assert over == correct
assert under > correct
def create_trials(mocker):
# Functions for recurrent object patching
def create_trials(mocker) -> None:
"""
When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return
false when looking for it
"""
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='freqtrade/tests/optimize/ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
return_value=False)
mocker.patch('freqtrade.optimize.hyperopt.save_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
return_value=True)
return mocker.Mock(
results=[{
'loss': 1,
'result': 'foo',
'status': 'ok'
}],
best_trial={'misc': {'vals': {'adx': 999}}}
_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.getsize', return_value=1)
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
return [{'loss': 1, 'result': 'foo', 'params': {}}]
def test_start(mocker, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker)
def test_start_calls_fmin(mocker):
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials)
mocker.patch('freqtrade.optimize.hyperopt.sorted',
return_value=trials.results)
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'hyperopt',
'--epochs', '5'
]
args = get_args(args)
StrategyResolver({'strategy': 'DefaultStrategy'})
start(args)
mock_fmin.assert_called_once()
import pprint
pprint.pprint(caplog.record_tuples)
assert log_has(
'Starting freqtrade in Hyperopt mode',
caplog.record_tuples
)
assert start_mock.call_count == 1
def test_start_uses_mongotrials(mocker):
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
hyperopt = _HYPEROPT
StrategyResolver({'strategy': 'DefaultStrategy'})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
start(args)
mock_mongotrials.assert_called_once()
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20)
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20)
assert over > correct
assert under > correct
def test_log_results_if_loss_improves(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
hyperopt = _HYPEROPT
shorter = hyperopt.calculate_loss(1, 100, 20)
longer = hyperopt.calculate_loss(1, 100, 30)
assert shorter < longer
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
hyperopt = _HYPEROPT
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
assert over == correct
assert under > correct
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
{
'loss': 1,
'current_tries': 1,
'total_tries': 2,
'result': 'foo'
})
logger.assert_called_once()
}
)
out, err = capsys.readouterr()
assert ' 1/2: foo. Loss 1.00000'in out
def test_no_log_if_loss_does_not_improve(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
{
'loss': 3,
})
assert not logger.called
}
)
assert caplog.record_tuples == []
def test_fmin_best_results(mocker, caplog):
fmin_result = {
"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,
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
hyperopt = _HYPEROPT
_HYPEROPT.trials = trials
hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Saving 1 evaluations to \'{}\''.format(trials_file),
caplog.record_tuples
)
mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
trials = create_trials(mocker)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Reading Trials from \'{}\''.format(trials_file),
caplog.record_tuples
)
assert hyperopt_trial == trials
mock_load.assert_called_once()
def test_roi_table_generation(init_hyperopt) -> None:
params = {
'roi_t1': 5,
'roi_t2': 10,
'roi_t3': 15,
'roi_p1': 1,
'roi_p2': 2,
'roi_p3': 3,
}
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
hyperopt = _HYPEROPT
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
args = mocker.Mock(epochs=1, config='config.json.example')
start(args)
exists = [
'Best parameters',
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"green_candle": {\n "enabled": true\n },',
'"mfi": {\n "enabled": false\n },',
'"trigger": {\n "type": "ao_cross_zero"\n },',
'"stoploss": -0.1',
def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', 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)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
parallel.assert_called_once()
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
assert dumper.called
def test_format_results(init_hyperopt):
"""
Test Hyperopt.format_results()
"""
# Test with BTC as stake_currency
trades = [
('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246)
]
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels)
for line in exists:
assert line in caplog.text
result = _HYPEROPT.format_results(df)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
def test_fmin_throw_value_error(mocker, caplog):
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
args = mocker.Mock(epochs=1, config='config.json.example')
start(args)
exists = [
'Best Result:',
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
'(param: -e).',
# Test with EUR as stake_currency
trades = [
('ETH/EUR', 2, 2, 123),
('LTC/EUR', 1, 1, 123),
('XPR/EUR', -1, -2, -246)
]
for line in exists:
assert line in caplog.text
df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df)
assert result.find('Total profit 1.00000000 EUR')
def test_resuming_previous_hyperopt_results_succeeds(mocker):
import freqtrade.optimize.hyperopt as hyperopt
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
return_value=trials)
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.len',
return_value=len(trials.results))
mock_read = mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=trials)
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.sorted',
return_value=trials.results)
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin',
return_value={})
args = mocker.Mock(epochs=1,
config='config.json.example',
mongodb=False)
def test_has_space(init_hyperopt):
"""
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')
start(args)
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))
_HYPEROPT.config.update({'spaces': ['all']})
assert _HYPEROPT.has_space('buy')
def test_save_trials_saves_trials(mocker):
trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump',
return_value=None)
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=trials_path)
save_trials(trials, trials_path)
def test_populate_indicators(init_hyperopt) -> None:
"""
Test Hyperopt.populate_indicators()
"""
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
mock_dump.assert_called_once_with(trials, trials_path)
# Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe
assert 'mfi' in dataframe
assert 'rsi' in dataframe
def test_read_trials_returns_trials_file(mocker):
trials = create_trials(mocker)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load',
return_value=trials)
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=mock_load)
def test_buy_strategy_generator(init_hyperopt) -> None:
"""
Test Hyperopt.buy_strategy_generator()
"""
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
assert read_trials() == trials
mock_open.assert_called_once()
mock_load.assert_called_once()
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{
'adx-value': 20,
'fastd-value': 20,
'mfi-value': 20,
'rsi-value': 20,
'adx-enabled': True,
'fastd-enabled': True,
'mfi-enabled': True,
'rsi-enabled': True,
'trigger': 'bb_lower'
}
)
result = populate_buy_trend(dataframe)
# Check if some indicators are generated. We will not test all of them
assert 'buy' in result
assert 1 in result['buy']
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
"""
Test Hyperopt.generate_optimizer() function
"""
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
trades = [
('POWR/BTC', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
MagicMock(return_value=backtest_result)
)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
optimizer_param = {
'adx-value': 0,
'fastd-value': 35,
'mfi-value': 0,
'rsi-value': 0,
'adx-enabled': False,
'fastd-enabled': True,
'mfi-enabled': False,
'rsi-enabled': False,
'trigger': 'macd_cross_signal',
'roi_t1': 60.0,
'roi_t2': 30.0,
'roi_t3': 20.0,
'roi_p1': 0.01,
'roi_p2': 0.01,
'roi_p3': 0.1,
'stoploss': -0.4,
}
response_expected = {
'loss': 1.9840569076926293,
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
'(0.0231Σ%). Avg duration 100.0 mins.',
'params': optimizer_param
}
hyperopt = Hyperopt(conf)
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected

View File

@ -1,16 +0,0 @@
# pragma pylint: disable=missing-docstring,W0212
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
def test_hyperopt_optimize_conf():
hyperopt_conf = hyperopt_optimize_conf()
assert "max_open_trades" in hyperopt_conf
assert "stake_currency" in hyperopt_conf
assert "stake_amount" in hyperopt_conf
assert "minimal_roi" in hyperopt_conf
assert "stoploss" in hyperopt_conf
assert "bid_strategy" in hyperopt_conf
assert "exchange" in hyperopt_conf
assert "pair_whitelist" in hyperopt_conf['exchange']

429
freqtrade/tests/optimize/test_optimize.py Normal file → Executable file
View File

@ -1,15 +1,24 @@
# pragma pylint: disable=missing-docstring,W0212
# pragma pylint: disable=missing-docstring, protected-access, C0103
import json
import os
import logging
import uuid
from shutil import copyfile
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\
download_backtesting_testdata, load_tickerdata_file
# Change this if modifying BTC_UNITEST testdatafile
_btc_unittest_length = 13681
import arrow
from freqtrade import optimize
from freqtrade.arguments import TimeRange
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
_BTC_UNITTEST_LENGTH = 13681
def _backup_file(file: str, copy_file: bool = False) -> None:
@ -43,134 +52,398 @@ def _clean_test_file(file: str) -> None:
os.rename(file_swp, file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
"""
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')
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_ETH'])
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 5 min'
) not in caplog.record_tuples
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
_clean_test_file(file)
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
"""
Test load_data() with 5 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='5m')
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 1 min'
) not in caplog.record_tuples
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 5m', caplog.record_tuples)
_clean_test_file(file)
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
"""
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_conf) -> None:
"""
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
# do not download a new pair if refresh_pairs isn't set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=False,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is False
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
'Use --refresh-pairs-cached to download the data',
caplog.record_tuples)
# download a new pair if refresh_pairs is set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=True,
exchange=exchange,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_MEME", Interval: 1 min'
) in caplog.record_tuples
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
def test_testdata_path():
def test_testdata_path() -> None:
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
def test_download_pairs(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
_backup_file(file1_1)
_backup_file(file1_5)
_backup_file(file2_1)
_backup_file(file2_5)
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI']) is True
assert os.path.isfile(file1_1) is False
assert os.path.isfile(file2_1) is False
assert download_pairs(None, exchange,
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_1) is True
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file2_1)
assert os.path.isfile(file1_5) is False
assert os.path.isfile(file2_5) is False
assert download_pairs(None, exchange,
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_5) is True
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
_clean_test_file(file2_1)
_clean_test_file(file2_5)
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
def test_load_cached_data_for_updating(mocker) -> None:
datadir = os.path.join(os.path.dirname(__file__), '..', 'testdata')
test_data = None
test_filename = os.path.join(datadir, 'UNITTEST_BTC-1m.json')
with open(test_filename, "rt") as file:
test_data = json.load(file)
# change now time to test 'line' cases
# now = last cached item + 1 hour
now_ts = test_data[-1][0] / 1000 + 60 * 60
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
# timeframe starts earlier than the cached data
# should fully update data
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == []
assert start_ts == test_data[0][0] - 1000
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
TimeRange(None, 'line', 0, -num_lines))
assert data == []
assert start_ts < test_data[0][0] - 1
# timeframe starts in the center of the cached data
# should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# timeframe starts after the chached data
# should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no timeframe is set
# should return the chached data w/o the last item
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no datafile exist
# should return timestamp start time
timerange = TimeRange('date', None, now_ts - 10000, 0)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - 10000) * 1000
# same with 'line' timeframe
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - num_lines * 60) * 1000
# no datafile exist, no timeframe is set
# should return an empty array and None
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
None)
assert data == []
assert start_ts is None
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error'))
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
exchange = get_patched_exchange(mocker, default_conf)
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.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')
_backup_file(file1_1)
_backup_file(file1_5)
download_pairs(None, pairs=['BTC-MEME'])
download_pairs(None, exchange, pairs=['MEME/BTC'], ticker_interval='1m')
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
assert ('freqtrade.optimize.__init__',
logging.INFO,
'Failed to download the pair: "BTC-MEME", Interval: 1 min'
) in caplog.record_tuples
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json')
_backup_file(file1)
download_backtesting_testdata(None, pair="BTC-XEL", interval=1)
download_backtesting_testdata(None, exchange, pair="XEL/BTC", tick_interval='1m')
assert os.path.isfile(file1) is True
_clean_test_file(file1)
# Download a 5 min ticker file
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json')
_backup_file(file2)
download_backtesting_testdata(None, pair="BTC-STORJ", interval=5)
download_backtesting_testdata(None, exchange, pair="STORJ/BTC", tick_interval='5m')
assert os.path.isfile(file2) is True
_clean_test_file(file2)
def test_load_tickerdata_file():
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
assert _btc_unittest_length == len(tickerdata)
def test_download_backtesting_testdata2(mocker, default_conf) -> None:
tick = [
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
]
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf)
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
assert json_dump_mock.call_count == 2
def test_load_tickerdata_file() -> None:
# 7 does not exist in either format.
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
# 1 exists only as a .json
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
def test_init(default_conf, mocker) -> None:
exchange = get_patched_exchange(mocker, default_conf)
assert {} == optimize.load_data(
'',
exchange=exchange,
pairs=[],
refresh_pairs=True,
ticker_interval=default_conf['ticker_interval']
)
def test_trim_tickerlist() -> None:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
with open(file) as data_file:
ticker_list = json.load(data_file)
ticker_list_len = len(ticker_list)
# Test the pattern ^(-\d+)$
# This pattern uses the latest N elements
timerange = TimeRange(None, 'line', 0, -5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[-1] is ticker[-1] # The last element must be the same
# Test the pattern ^(\d+)-$
# This pattern keep X element from the end
timerange = TimeRange('line', None, 5, 0)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is ticker[0] # The first element must be the same
assert ticker_list[-1] is not ticker[-1] # The last element should be different
# Test the pattern ^(\d+)-(\d+)$
# This pattern extract a window
timerange = TimeRange('index', 'index', 5, 10)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^(\d{8})-(\d{8})$
# This pattern extract a window between the dates
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^-(\d{8})$
# This pattern extracts elements from the start to the date
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 10
assert ticker_list[0] is ticker[0] # The start of the list is included
assert ticker_list[9] is ticker[-1] # The element 10 is not included
# Test the pattern ^(\d{8})-$
# This pattern extracts elements from the date to now
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == ticker_list_len - 10
assert ticker_list[10] is ticker[0] # The first element is element #10
assert ticker_list[-1] is ticker[-1] # The last element is the same
# Test a wrong pattern
# This pattern must return the list unchanged
timerange = TimeRange(None, None, None, 5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_list_len == ticker_len
def test_file_dump_json() -> None:
"""
Test file_dump_json()
:return: None
"""
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
'test_{id}.json'.format(id=str(uuid.uuid4())))
data = {'bar': 'foo'}
# check the file we will create does not exist
assert os.path.isfile(file) is False
# Create the Json file
file_dump_json(file, data)
# Check the file was create
assert os.path.isfile(file) is True
# Open the Json file created and test the data is in it
with open(file) as data_file:
json_from_file = json.load(data_file)
assert 'bar' in json_from_file
assert json_from_file['bar'] == 'foo'
# Remove the file
_clean_test_file(file)

590
freqtrade/tests/rpc/test_rpc.py Normal file → Executable file
View File

@ -1,57 +1,565 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
from copy import deepcopy
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
"""
Unit test file for rpc/rpc.py
"""
from datetime import datetime
from unittest.mock import MagicMock
from freqtrade.rpc import init, cleanup, send_msg
import pytest
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap,
patch_get_signal)
def test_init_telegram_enabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
init(default_conf)
assert telegram_mock.call_count == 1
assert 'telegram' in module_list
# Functions for recurrent object patching
def prec_satoshi(a, b) -> float:
"""
:return: True if A and B differs less than one satoshi.
"""
return abs(a - b) < 0.00000001
def test_init_telegram_disabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
# Unit tests
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)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
init(conf)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
assert telegram_mock.call_count == 0
assert 'telegram' not in module_list
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_trade_status()
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_trade_status()
freqtradebot.create_trade()
trades = rpc._rpc_trade_status()
trade = trades[0]
result_message = [
'*Trade ID:* `1`\n'
'*Current Pair:* '
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'*Open Since:* `just now`\n'
'*Amount:* `90.99181074`\n'
'*Open Rate:* `0.00001099`\n'
'*Close Rate:* `None`\n'
'*Current Rate:* `0.00001098`\n'
'*Close Profit:* `None`\n'
'*Current Profit:* `-0.59%`\n'
'*Open Order:* `(limit buy rem=0.00000000)`'
]
assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0
def test_cleanup_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 1
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)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
rpc._rpc_status_table()
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
rpc._rpc_status_table()
freqtradebot.create_trade()
result = rpc._rpc_status_table()
assert 'just now' in result['Since'].all()
assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all()
def test_cleanup_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 0
def test_rpc_daily_profit(default_conf, update, ticker, fee,
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})
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.create_trade()
trade = Trade.query.first()
assert trade
# Simulate buy & sell
trade.update(limit_buy_order)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
update.message.text = '/daily 2'
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days) == 7
for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
assert (day[1] == '0.00000000 BTC' or
day[1] == '0.00006217 BTC')
assert (day[2] == '0.000 USD' or
day[2] == '0.933 USD')
# ensure first day is current date
assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
def test_send_msg_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 1
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_trade_statistics() method
"""
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
with pytest.raises(RPCException, match=r'.*no closed trade*'):
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
# Create some test data
freqtradebot.create_trade()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
freqtradebot.create_trade()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.81)
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
def test_send_msg_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 0
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
ticker_sell_up, limit_buy_order, limit_sell_order):
"""
Test rpc_trade_statistics() method
"""
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.create_trade()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up,
get_fee=fee
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0)
assert prec_satoshi(stats['profit_all_coin'], 0)
assert prec_satoshi(stats['profit_all_percent'], 0)
assert prec_satoshi(stats['profit_all_fiat'], 0)
assert stats['trade_count'] == 1
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
def test_rpc_balance_handle(default_conf, mocker):
"""
Test rpc_balance() method
"""
mock_balance = {
'BTC': {
'free': 10.0,
'total': 12.0,
'used': 2.0,
},
'ETH': {
'free': 0.0,
'total': 0.0,
'used': 0.0,
}
}
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=mock_balance)
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert prec_satoshi(total, 12)
assert prec_satoshi(value, 180000)
assert 'USD' in symbol
assert len(output) == 1
assert 'BTC' in output[0]['currency']
assert prec_satoshi(output[0]['available'], 10)
assert prec_satoshi(output[0]['balance'], 12)
assert prec_satoshi(output[0]['pending'], 2)
assert prec_satoshi(output[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None:
"""
Test rpc_start() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
result = rpc._rpc_start()
assert '`Starting trader ...`' in result
assert freqtradebot.state == State.RUNNING
result = rpc._rpc_start()
assert '*Status:* `already running`' in result
assert freqtradebot.state == State.RUNNING
def test_rpc_stop(mocker, default_conf) -> None:
"""
Test rpc_stop() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
result = rpc._rpc_stop()
assert '`Stopping trader ...`' in result
assert freqtradebot.state == State.STOPPED
result = rpc._rpc_stop()
assert '*Status:* `already stopped`' in result
assert freqtradebot.state == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
"""
Test rpc_forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(
return_value={
'status': 'closed',
'type': 'limit',
'side': 'buy'
}
),
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell(None)
freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
rpc._rpc_forcesell(None)
rpc._rpc_forcesell('all')
freqtradebot.create_trade()
rpc._rpc_forcesell('all')
rpc._rpc_forcesell('1')
freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell(None)
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell('all')
freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': filled_amount
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated
rpc._rpc_forcesell('1')
assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
freqtradebot.create_trade()
trade = Trade.query.filter(Trade.id == '2').first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
rpc._rpc_forcesell('2')
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
freqtradebot.create_trade()
# make an limit-sell open trade
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'sell'
}
)
rpc._rpc_forcesell('3')
# status quo, no exchange calls
assert cancel_order_mock.call_count == 2
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, markets, mocker) -> None:
"""
Test rpc_performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
# Create some test data
freqtradebot.create_trade()
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
res = rpc._rpc_performance()
assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
"""
Test rpc_count() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
trades = rpc._rpc_count()
nb_trades = len(trades)
assert nb_trades == 0
# Create some test data
freqtradebot.create_trade()
trades = rpc._rpc_count()
nb_trades = len(trades)
assert nb_trades == 1

View File

@ -0,0 +1,123 @@
"""
Unit test file for rpc/rpc_manager.py
"""
import logging
from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
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:
""" Test __init__() method """
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert rpc_manager.registered_modules == []
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
""" Test _init() method with Telegram disabled """
caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == []
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test _init() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.cleanup()
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert telegram_mock.call_count == 0
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1

1413
freqtrade/tests/rpc/test_rpc_telegram.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
import json
import pytest
from pandas import DataFrame
from freqtrade.analyze import Analyze
from freqtrade.strategy.default_strategy import DefaultStrategy
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file))
def test_default_strategy_structure():
assert hasattr(DefaultStrategy, 'minimal_roi')
assert hasattr(DefaultStrategy, 'stoploss')
assert hasattr(DefaultStrategy, 'ticker_interval')
assert hasattr(DefaultStrategy, 'populate_indicators')
assert hasattr(DefaultStrategy, 'populate_buy_trend')
assert hasattr(DefaultStrategy, 'populate_sell_trend')
def test_default_strategy(result):
strategy = DefaultStrategy()
assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result)
assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame
assert type(strategy.populate_sell_trend(indicators)) is DataFrame

View File

@ -0,0 +1,145 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103
import logging
import os
import pytest
from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.resolver import StrategyResolver
def test_import_strategy(caplog):
caplog.set_level(logging.DEBUG)
strategy = DefaultStrategy()
strategy.some_method = lambda *args, **kwargs: 42
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
assert strategy.some_method() == 42
imported_strategy = import_strategy(strategy)
assert dir(strategy) == dir(imported_strategy)
assert imported_strategy.__module__ == 'freqtrade.strategy'
assert imported_strategy.some_method() == 42
assert (
'freqtrade.strategy',
logging.DEBUG,
'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
'as freqtrade.strategy.DefaultStrategy',
) in caplog.record_tuples
def test_search_strategy():
default_location = os.path.join(os.path.dirname(
os.path.realpath(__file__)), '..', '..', 'strategy'
)
assert isinstance(
StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy
)
assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None
def test_load_strategy(result):
resolver = StrategyResolver({'strategy': 'TestStrategy'})
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path')
resolver._load_strategy('TestStrategy', extra_dir)
assert (
'freqtrade.strategy.resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_not_found_strategy():
strategy = StrategyResolver()
with pytest.raises(ImportError,
match=r'Impossible to load Strategy \'NotFoundStrategy\'.'
r' This class does not exist or contains Python code errors'):
strategy._load_strategy('NotFoundStrategy')
def test_strategy(result):
resolver = StrategyResolver({'strategy': 'DefaultStrategy'})
assert hasattr(resolver.strategy, 'minimal_roi')
assert resolver.strategy.minimal_roi[0] == 0.04
assert hasattr(resolver.strategy, 'stoploss')
assert resolver.strategy.stoploss == -0.10
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
assert hasattr(resolver.strategy, 'populate_buy_trend')
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result))
assert 'buy' in dataframe.columns
assert hasattr(resolver.strategy, 'populate_sell_trend')
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
assert 'sell' in dataframe.columns
def test_strategy_override_minimal_roi(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'minimal_roi': {
"0": 0.5
}
}
resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'minimal_roi')
assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.strategy.resolver',
logging.INFO,
'Override strategy \'minimal_roi\' with value in config file.'
) in caplog.record_tuples
def test_strategy_override_stoploss(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'stoploss': -0.5
}
resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'stoploss')
assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.strategy.resolver',
logging.INFO,
'Override strategy \'stoploss\' with value in config file: -0.5.'
) in caplog.record_tuples
def test_strategy_override_ticker_interval(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'ticker_interval': 60
}
resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'ticker_interval')
assert resolver.strategy.ticker_interval == 60
assert ('freqtrade.strategy.resolver',
logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.'
) in caplog.record_tuples

159
freqtrade/tests/test_acl_pair.py Normal file → Executable file
View File

@ -1,4 +1,8 @@
from freqtrade.main import refresh_whitelist, gen_pair_whitelist
# pragma pylint: disable=missing-docstring,C0103,protected-access
from unittest.mock import MagicMock
import freqtrade.tests.conftest as tt # test tools
# whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine
@ -6,136 +10,81 @@ from freqtrade.main import refresh_whitelist, gen_pair_whitelist
def whitelist_conf():
return {
'stake_currency': 'BTC',
'exchange': {
'pair_whitelist': [
'BTC_ETH',
'BTC_TKN',
'BTC_TRST',
'BTC_SWT',
'BTC_BCC'
],
'pair_blacklist': [
'BTC_BLK'
],
},
}
config = tt.default_conf()
def get_market_summaries():
return [{
'MarketName': 'BTC-TKN',
'High': 0.00000919,
'Low': 0.00000820,
'Volume': 74339.61396015,
'Last': 0.00000820,
'BaseVolume': 1664,
'TimeStamp': '2014-07-09T07:19:30.15',
'Bid': 0.00000820,
'Ask': 0.00000831,
'OpenBuyOrders': 15,
'OpenSellOrders': 15,
'PrevDay': 0.00000821,
'Created': '2014-03-20T06:00:00',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-ETH',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 42,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-BLK',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 3,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}]
def get_health():
return [{'Currency': 'ETH',
'IsActive': True
},
{'Currency': 'TKN',
'IsActive': True
},
{'Currency': 'BLK',
'IsActive': True
}
config['stake_currency'] = 'BTC'
config['exchange']['pair_whitelist'] = [
'ETH/BTC',
'TKN/BTC',
'TRST/BTC',
'SWT/BTC',
'BCC/BTC'
]
config['exchange']['pair_blacklist'] = [
'BLK/BTC'
]
def get_health_empty():
return []
return config
def test_refresh_market_pair_not_in_whitelist(mocker):
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['BTC_XXX'])
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
)
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist(mocker):
def test_refresh_whitelist(mocker, markets):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic(mocker):
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
mocker.patch.multiple('freqtrade.main.exchange',
get_market_summaries=get_market_summaries)
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_markets=markets,
get_tickers=tickers,
exchange_has=MagicMock(return_value=True)
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_TKN', 'BTC_ETH']
refreshedwhitelist = refresh_whitelist(
gen_pair_whitelist(conf['stake_currency']))
whitelist = ['ETH/BTC', 'TKN/BTC']
refreshedwhitelist = freqtradebot._refresh_whitelist(
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
)
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker):
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health_empty)
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
# argument: use the whitelist dynamically by exchange-volume
whitelist = []
conf['exchange']['pair_whitelist'] = []
refresh_whitelist(whitelist)
freqtradebot._refresh_whitelist(whitelist)
pairslist = conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist)

214
freqtrade/tests/test_analyze.py Normal file → Executable file
View File

@ -1,74 +1,198 @@
# pragma pylint: disable=missing-docstring,W0621
import json
# pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for analyse.py
"""
import logging
from unittest.mock import MagicMock
import arrow
import pytest
from pandas import DataFrame
from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe,
populate_buy_trend, populate_indicators,
populate_sell_trend)
from freqtrade.analyze import Analyze, SignalType
from freqtrade.arguments import TimeRange
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.tests.conftest import get_patched_exchange, log_has
# Avoid to reinit the same object again and again
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file))
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')
assert hasattr(Analyze, 'stop_loss_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() == \
['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_correct_length(result):
assert len(result.index) == 14395
['date', 'open', 'high', 'low', 'close', 'volume']
def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result))
# Load the default strategy for the unit test, because this logic is done in main.py
dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
assert 'buy' in dataframe.columns
def test_populates_sell_trend(result):
dataframe = populate_sell_trend(populate_indicators(result))
# Load the default strategy for the unit test, because this logic is done in main.py
dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
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 get_signal('BTC-ETH', SignalType.BUY)
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.BUY)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
def test_returns_latest_sell_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.SELL)
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.SELL)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
def test_get_signal_handles_exceptions(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker',
side_effect=Exception('invalid ticker history '))
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 not get_signal('BTC-ETH', SignalType.BUY)
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)
# 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.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 16 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

192
freqtrade/tests/test_arguments.py Executable file
View File

@ -0,0 +1,192 @@
# pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for arguments.py
"""
import argparse
import logging
import pytest
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
def test_parse_args_none() -> None:
arguments = Arguments([], '')
assert isinstance(arguments, Arguments)
assert isinstance(arguments.parser, argparse.ArgumentParser)
assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None:
args = Arguments([], '').get_parsed_arg()
assert args.config == 'config.json'
assert args.dynamic_whitelist is None
assert args.loglevel == logging.INFO
def test_parse_args_config() -> None:
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
assert args.config == '/dev/null'
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
assert args.config == '/dev/null'
def test_parse_args_db_url() -> None:
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
assert args.db_url == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
def test_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options()
args = arguments.get_parsed_arg()
assert args.pair == 'ETH/BTC'
def test_parse_args_version() -> None:
with pytest.raises(SystemExit, match=r'0'):
Arguments(['--version'], '').get_parsed_arg()
def test_parse_args_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['-c'], '').get_parsed_arg()
def test_parse_args_strategy() -> None:
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
assert args.strategy == 'SomeStrategy'
def test_parse_args_strategy_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy'], '').get_parsed_arg()
def test_parse_args_strategy_path() -> None:
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
assert args.strategy_path == '/some/path'
def test_parse_args_strategy_path_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy-path'], '').get_parsed_arg()
def test_parse_args_dynamic_whitelist() -> None:
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10() -> None:
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
def test_parse_timerange_incorrect() -> None:
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
timerange = Arguments.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
# TODO: Find solution for the following case (passing timestamp in ms)
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
def test_parse_args_backtesting_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
def test_parse_args_backtesting_custom() -> None:
args = [
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1m',
'--refresh-pairs-cached']
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == '1m'
assert call_args.refresh_pairs is True
def test_parse_args_hyperopt_custom() -> None:
args = [
'-c', 'test_conf.json',
'hyperopt',
'--epochs', '20',
'--spaces', 'buy'
]
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy']
assert call_args.func is not None
def test_testdata_dl_options() -> None:
args = [
'--pairs-file', 'file_with_pairs',
'--export', 'export/folder',
'--days', '30',
'--exchange', 'binance'
]
arguments = Arguments(args, '')
arguments.testdata_dl_options()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs'
assert args.export == 'export/folder'
assert args.days == 30
assert args.exchange == 'binance'

View File

@ -0,0 +1,404 @@
# pragma pylint: disable=protected-access, invalid-name
"""
Unit test file for configuration.py
"""
import json
from argparse import Namespace
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from jsonschema import ValidationError
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.tests.conftest import log_has
def test_configuration_object() -> None:
"""
Test the Constants object has the mandatory Constants
"""
assert hasattr(Configuration, 'load_config')
assert hasattr(Configuration, '_load_config_file')
assert hasattr(Configuration, '_validate_config')
assert hasattr(Configuration, '_load_common_config')
assert hasattr(Configuration, '_load_backtesting_config')
assert hasattr(Configuration, '_load_hyperopt_config')
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf) -> None:
"""
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.*'):
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_missing_attributes(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None:
"""
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\'.*'):
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_file(default_conf, mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
"""
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
configuration = Configuration(Namespace())
validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
assert 'internals' in validated_conf
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
"""
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(
read_data=json.dumps(conf)
))
Configuration(Namespace())._load_config_file('somefile')
assert file_mock.call_count == 1
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker) -> None:
"""
Test Configuration._load_config_file() method
"""
mocker.patch(
'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found'))
)
configuration = Configuration(Namespace())
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
configuration._load_config_file('somefile')
def test_load_config(default_conf, mocker) -> None:
"""
Test Configuration.load_config() without any cli params
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('strategy') == 'DefaultStrategy'
assert validated_conf.get('strategy_path') is None
assert 'dynamic_whitelist' not in validated_conf
def test_load_config_with_params(default_conf, mocker) -> None:
"""
Test Configuration.load_config() with cli params used
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path',
'--db-url', 'sqlite:///someurl',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('dynamic_whitelist') == 10
assert validated_conf.get('strategy') == 'TestStrategy'
assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('db_url') == 'sqlite:///someurl'
conf = default_conf.copy()
conf["dry_run"] = False
del conf["db_url"]
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
# Test dry=run with ProdURL
conf = default_conf.copy()
conf["dry_run"] = True
conf["db_url"] = DEFAULT_DB_PROD_URL
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
def test_load_custom_strategy(default_conf, mocker) -> None:
"""
Test Configuration.load_config() without any cli params
"""
custom_conf = deepcopy(default_conf)
custom_conf.update({
'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies',
})
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(custom_conf)
))
args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('strategy') == 'CustomStrategy'
assert validated_conf.get('strategy_path') == '/tmp/strategies'
def test_show_info(default_conf, mocker, caplog) -> None:
"""
Test Configuration.show_info()
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--db-url', 'sqlite:///tmp/testdb',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
configuration.get_config()
assert log_has(
'Parameter --dynamic-whitelist detected. '
'Using dynamically generated whitelist. '
'(not applicable with Backtesting and Hyperopt)',
caplog.record_tuples
)
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', caplog.record_tuples)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
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 not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' not in config
assert 'export' not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
]
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 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' in config
assert log_has(
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [
'hyperopt',
'--epochs', '10',
'--spaces', 'all',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'epochs' in config
assert int(config['epochs']) == 10
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
assert 'spaces' in config
assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration(Namespace())
# Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf)
# Test a valid exchange
conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf)
# Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf
with pytest.raises(
OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*'
):
configuration.check_exchange(conf)

View File

@ -0,0 +1,25 @@
"""
Unit test file for constants.py
"""
from freqtrade import constants
def test_constant_object() -> None:
"""
Test the Constants object has the mandatory Constants
"""
assert hasattr(constants, 'CONF_SCHEMA')
assert hasattr(constants, 'DYNAMIC_WHITELIST')
assert hasattr(constants, 'PROCESS_THROTTLE_SECS')
assert hasattr(constants, 'TICKER_INTERVAL')
assert hasattr(constants, 'HYPEROPT_EPOCH')
assert hasattr(constants, 'RETRY_TIMEOUT')
assert hasattr(constants, 'DEFAULT_STRATEGY')
def test_conf_schema() -> None:
"""
Test the CONF_SCHEMA is from the right type
"""
assert isinstance(constants.CONF_SCHEMA, dict)

15
freqtrade/tests/test_dataframe.py Normal file → Executable file
View File

@ -1,26 +1,33 @@
# pragma pylint: disable=missing-docstring, C0103
import pandas
import freqtrade.optimize
from freqtrade import analyze
from freqtrade.analyze import Analyze
from freqtrade.optimize import load_data
from freqtrade.strategy.resolver import StrategyResolver
_pairs = ['BTC_ETH']
_pairs = ['ETH/BTC']
def load_dataframe_pair(pairs):
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs)
ld = load_data(None, ticker_interval='5m', pairs=pairs)
assert isinstance(ld, dict)
assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]]
analyze = Analyze({'strategy': 'DefaultStrategy'})
dataframe = analyze.analyze_ticker(dataframe)
return dataframe
def test_dataframe_load():
StrategyResolver({'strategy': 'DefaultStrategy'})
dataframe = load_dataframe_pair(_pairs)
assert isinstance(dataframe, pandas.core.frame.DataFrame)
def test_dataframe_columns_exists():
StrategyResolver({'strategy': 'DefaultStrategy'})
dataframe = load_dataframe_pair(_pairs)
assert 'high' in dataframe.columns
assert 'low' in dataframe.columns

118
freqtrade/tests/test_fiat_convert.py Normal file → Executable file
View File

@ -1,11 +1,14 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors,
# pragma pylint: disable=protected-access, C0103
import time
from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
def test_pair_convertion_object():
@ -36,7 +39,8 @@ def test_pair_convertion_object():
assert pair_convertion.price == 30000.123
def test_fiat_convert_is_supported():
def test_fiat_convert_is_supported(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._is_supported_fiat(fiat='USD') is True
assert fiat_convert._is_supported_fiat(fiat='usd') is True
@ -44,35 +48,39 @@ def test_fiat_convert_is_supported():
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
def test_fiat_convert_add_pair():
def test_fiat_convert_add_pair(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._pairs) == 0
pair_len = len(fiat_convert._pairs)
assert pair_len == 0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
assert len(fiat_convert._pairs) == 1
pair_len = len(fiat_convert._pairs)
assert pair_len == 1
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 12345.0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
assert len(fiat_convert._pairs) == 2
pair_len = len(fiat_convert._pairs)
assert pair_len == 2
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
assert fiat_convert._pairs[1].price == 13000.2
def test_fiat_convert_find_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 12345.0,
'price_eur': 13000.2
})
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
@ -81,12 +89,17 @@ def test_fiat_convert_find_price(mocker):
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
def test_fiat_convert_get_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 28000.0,
'price_eur': 15000.0
})
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
fiat_convert = CryptoToFiatConverter()
@ -95,7 +108,8 @@ def test_fiat_convert_get_price(mocker):
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
# Check the value return by the method
assert len(fiat_convert._pairs) == 0
pair_len = len(fiat_convert._pairs)
assert pair_len == 0
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
@ -116,10 +130,74 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_without_network(mocker):
pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!'))
mocker.patch('freqtrade.fiat_convert.Pymarketcap', pymarketcap)
def test_fiat_convert_same_currencies(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
def test_fiat_convert_two_FIAT(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2
assert fiat_convert._cryptomap["BTC"] == "1"
def test_fiat_init_network_exception(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(side_effect=RequestException)
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
def test_fiat_convert_without_network(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
cmc_temp = CryptoToFiatConverter._coinmarketcap
CryptoToFiatConverter._coinmarketcap = None
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
CryptoToFiatConverter._coinmarketcap = cmc_temp
def test_convert_amount(mocker):
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
fiat_convert = CryptoToFiatConverter()
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="USD"
)
assert result == 15184.35
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="BTC"
)
assert result == 1.23

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,13 @@
import pandas as pd
from freqtrade.indicator_helpers import went_down, went_up
def test_went_up():
series = pd.Series([1, 2, 3, 1])
assert went_up(series).equals(pd.Series([False, True, True, False]))
def test_went_down():
series = pd.Series([1, 2, 3, 1])
assert went_down(series).equals(pd.Series([False, False, False, True]))

797
freqtrade/tests/test_main.py Normal file → Executable file
View File

@ -1,31 +1,28 @@
# pragma pylint: disable=missing-docstring,C0103
import copy
"""
Unit test file for main.py
"""
import logging
from copy import deepcopy
from unittest.mock import MagicMock
import arrow
import pytest
import requests
from sqlalchemy import create_engine
import freqtrade.main as main
from freqtrade import DependencyException, OperationalException
from freqtrade.analyze import SignalType
from freqtrade.exchange import Exchanges
from freqtrade.main import (_process, check_handle_timedout, create_trade,
execute_sell, get_target_bid, handle_trade, init)
from freqtrade.misc import State, get_state
from freqtrade.persistence import Trade
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, reconfigure, set_loggers
from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_exchange
def test_parse_args_backtesting(mocker):
""" Test that main() can start backtesting or hyperopt.
and also ensure we can pass some specific arguments
argument parsing is done in test_misc.py """
backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['backtesting'])
def test_parse_args_backtesting(mocker) -> None:
"""
Test that main() can start backtesting and also ensure we can pass some specific arguments
further argument parsing is done in test_arguments.py
"""
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
main(['backtesting'])
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json'
@ -33,14 +30,15 @@ def test_parse_args_backtesting(mocker):
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 5
assert call_args.ticker_interval is None
def test_main_start_hyperopt(mocker):
hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['hyperopt'])
def test_main_start_hyperopt(mocker) -> None:
"""
Test that main() can start hyperopt
"""
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
main(['hyperopt'])
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json'
@ -49,628 +47,171 @@ def test_main_start_hyperopt(mocker):
assert call_args.func is not None
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://'))
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
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
set_loggers()
result = _process()
assert result is True
value1 = logging.getLogger('requests.packages.urllib3').level
assert previous_value1 is not value1
assert value1 is logging.INFO
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
trade = trades[0]
assert trade is not None
assert trade.stake_amount == default_conf['stake_amount']
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073703367
value2 = logging.getLogger('telegram').level
assert previous_value2 is not value2
assert value2 is logging.INFO
def test_process_exchange_failures(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://'))
result = _process()
assert result is False
assert sleep_mock.has_calls()
def test_process_operational_exception(default_conf, ticker, health, mocker):
msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=OperationalException))
init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING
result = _process()
assert result is False
assert get_state() == State.STOPPED
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal',
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
result = _process()
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
result = _process()
assert result is False
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
# Save state of current whitelist
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
buy_mock = mocker.patch(
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=Exception),
cleanup=MagicMock(),
)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
min_stake_amount = 0.0005
create_trade(min_stake_amount)
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= min_stake_amount
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Fatal exception!', caplog.record_tuples)
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount'])
def test_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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=KeyboardInterrupt),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Oh snap!', caplog.record_tuples)
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(return_value=State.RELOAD_CONF),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell'))
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
assert trade.is_open is True
handle_trade(trade)
assert trade.open_order_id == 'mocked_limit_sell'
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.06201057
assert trade.calc_profit() == 0.00006217
assert trade.close_date is not None
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell
# instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without
# executing
# if ROI is reached we must sell
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
# if ROI is reached we must sell even if sell-signal is not signalled
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
value_returned = handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
assert value_returned is False
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
s = 'Executing sell due to sell signal ...'
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
# Create trade and sell it
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
trade.update(limit_sell_order)
assert trade.is_open is False
with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade)
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
# Raise exception as side effect to avoid endless loop
reconfigure_mock = mocker.patch(
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
)
Trade.session.add(trade_buy)
with pytest.raises(SystemExit):
main(['-c', 'config.json.example'])
# check it does cancel buy orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 0
assert reconfigure_mock.call_count == 1
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
def test_reconfigure(mocker, default_conf) -> None:
""" Test recreate() function """
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
trade_sell = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=False
freqtrade = FreqtradeBot(default_conf)
# Renew mock to return modified data
conf = deepcopy(default_conf)
conf['stake_amount'] += 1
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert trade_sell.is_open is True
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
# reconfigure should return a new instance
freqtrade2 = reconfigure(
freqtrade,
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
)
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20
def test_balance_fully_last_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 10
def test_balance_bigger_last_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
mocker.patch('freqtrade.main._CONF', {})
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True
# Verify we have a new instance with the new config
assert freqtrade is not freqtrade2
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']

225
freqtrade/tests/test_misc.py Normal file → Executable file
View File

@ -1,164 +1,93 @@
# pragma pylint: disable=missing-docstring,C0103
import argparse
import json
import time
from copy import deepcopy
import pytest
from jsonschema import ValidationError
"""
Unit test file for misc.py
"""
from freqtrade.misc import (common_args_parser, load_config, parse_args,
throttle)
import datetime
from unittest.mock import MagicMock
from freqtrade.analyze import Analyze
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
file_dump_json, format_ms_time, shorten_date)
from freqtrade.optimize.__init__ import load_tickerdata_file
def test_throttle():
def func():
return 42
start = time.time()
result = throttle(func, min_secs=0.1)
end = time.time()
assert result == 42
assert end - start > 0.1
result = throttle(func, min_secs=-1)
assert result == 42
def test_shorten_date() -> None:
"""
Test shorten_date() function
:return: None
"""
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
assert shorten_date(str_data) == str_shorten_data
def test_throttle_with_assets():
def test_datesarray_to_datetimearray(ticker_history):
"""
Test datesarray_to_datetimearray() function
:return: None
"""
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
dates = datesarray_to_datetimearray(dataframes['date'])
def func(nb_assets=-1):
return nb_assets
assert isinstance(dates[0], datetime.datetime)
assert dates[0].year == 2017
assert dates[0].month == 11
assert dates[0].day == 26
assert dates[0].hour == 8
assert dates[0].minute == 50
result = throttle(func, min_secs=0.1, nb_assets=666)
assert result == 666
result = throttle(func, min_secs=0.1)
assert result == -1
date_len = len(dates)
assert date_len == 2
# Parse common command-line-arguments. Used for all tools
def test_common_datearray(default_conf) -> None:
"""
Test common_datearray()
:return: None
"""
analyze = Analyze(default_conf)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
def test_parse_args_none():
args = common_args_parser('')
assert isinstance(args, argparse.ArgumentParser)
dates = common_datearray(dataframes)
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1]
def test_parse_args_defaults():
args = parse_args([], '')
assert args.config == 'config.json'
assert args.dynamic_whitelist is None
assert args.loglevel == 20
def test_file_dump_json(mocker) -> None:
"""
Test file_dump_json()
:return: None
"""
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True)
assert file_open.call_count == 1
assert json_dump.call_count == 1
def test_parse_args_config():
args = parse_args(['-c', '/dev/null'], '')
assert args.config == '/dev/null'
args = parse_args(['--config', '/dev/null'], '')
assert args.config == '/dev/null'
def test_parse_args_verbose():
args = parse_args(['-v'], '')
assert args.loglevel == 10
args = parse_args(['--verbose'], '')
assert args.loglevel == 10
def test_parse_args_version():
with pytest.raises(SystemExit, match=r'0'):
parse_args(['--version'], '')
def test_parse_args_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['-c'], '')
# Parse command-line-arguments
# used for main, backtesting and hyperopt
def test_parse_args_dynamic_whitelist():
args = parse_args(['--dynamic-whitelist'], '')
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10():
args = parse_args(['--dynamic-whitelist', '10'], '')
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['--dynamic-whitelist', 'abc'], '')
def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval'], '')
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval', 'abc'], '')
def test_parse_args_backtesting_custom():
args = [
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--refresh-pairs-cached']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.refresh_pairs is True
def test_parse_args_hyperopt_custom(mocker):
args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
validated_conf = load_config('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
def test_load_config_invalid_pair(default_conf, mocker):
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*does not match.*'):
load_config('somefile')
def test_load_config_missing_attributes(default_conf, mocker):
conf = deepcopy(default_conf)
conf.pop('exchange')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
load_config('somefile')
def test_format_ms_time() -> None:
"""
test format_ms_time()
:return: None
"""
# Date 2018-04-10 18:02:01
date_in_epoch_ms = 1523383321000
date = format_ms_time(date_in_epoch_ms)
assert type(date) is str
res = datetime.datetime(2018, 4, 10, 18, 2, 1, tzinfo=datetime.timezone.utc)
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
res = datetime.datetime(2017, 12, 13, 8, 2, 1, tzinfo=datetime.timezone.utc)
# Date 2017-12-13 08:02:01
date_in_epoch_ms = 1513152121000
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')

425
freqtrade/tests/test_persistence.py Normal file → Executable file
View File

@ -1,95 +1,74 @@
# pragma pylint: disable=missing-docstring
import os
# pragma pylint: disable=missing-docstring, C0103
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade, init
from freqtrade import OperationalException, constants
from freqtrade.persistence import Trade, clean_dry_run_db, init
from freqtrade.tests.conftest import log_has
def test_init_create_session(default_conf, mocker):
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
@pytest.fixture(scope='function')
def init_persistence(default_conf):
init(default_conf)
def test_init_create_session(default_conf):
# Check if init create a session
init(default_conf)
assert hasattr(Trade, 'session')
assert type(Trade.session).__name__ is 'Session'
assert 'Session' in type(Trade.session).__name__
def test_init_dry_run_db(default_conf, mocker):
default_conf.update({'dry_run_db': True})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_custom_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is True
# Delete the file made for this unitest and rollback to the previous
# tradesv3.dry_run.sqlite file
# 1. Delete file from the test
if os.path.isfile(dry_run_db):
os.remove(dry_run_db)
# 2. Rollback to the initial file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_dry_run_without_db(default_conf, mocker):
default_conf.update({'dry_run_db': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_invalid_db_url(default_conf):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is False
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf)
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
conf = deepcopy(default_conf)
conf.update({'dry_run': False})
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
prod_db = 'tradesv3.sqlite'
prod_db_swp = prod_db + '.swp'
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
if os.path.isfile(prod_db):
os.rename(prod_db, prod_db_swp)
# Check if the new tradesv3.sqlite was created
init(default_conf)
assert os.path.isfile(prod_db) is True
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
# 1. Delete file from the test
if os.path.isfile(prod_db):
os.remove(prod_db)
# Rollback to the initial 'tradesv3.sqlite' file
if os.path.isfile(prod_db_swp):
os.rename(prod_db_swp, prod_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
def test_init_dryrun_db(default_conf, mocker):
conf = deepcopy(default_conf)
conf.update({'dry_run': True})
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
@pytest.mark.usefixtures("init_persistence")
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
"""
On this test we will buy and sell a crypto currency.
@ -118,10 +97,11 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
assert trade.open_order_id is None
assert trade.open_rate is None
@ -143,12 +123,14 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
assert trade.close_date is not None
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@ -165,12 +147,14 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
assert trade.calc_profit_percent() == 0.06201057
def test_calc_close_trade_price_exception(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_exception(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@ -178,12 +162,14 @@ def test_calc_close_trade_price_exception(limit_buy_order):
assert trade.calc_close_trade_price() == 0.0
@pytest.mark.usefixtures("init_persistence")
def test_update_open_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
assert trade.open_order_id is None
@ -191,7 +177,7 @@ def test_update_open_order(limit_buy_order):
assert trade.close_profit is None
assert trade.close_date is None
limit_buy_order['closed'] = False
limit_buy_order['status'] = 'open'
trade.update(limit_buy_order)
assert trade.open_order_id is None
@ -200,24 +186,28 @@ def test_update_open_order(limit_buy_order):
assert trade.close_date is None
@pytest.mark.usefixtures("init_persistence")
def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order)
def test_calc_open_trade_price(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_price(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -229,12 +219,14 @@ def test_calc_open_trade_price(limit_buy_order):
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -250,12 +242,14 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
def test_calc_profit(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -272,10 +266,6 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
# Lower than open rate
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit() == 0.00006217
@ -284,12 +274,14 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
assert trade.calc_profit(fee=0.003) == 0.00006163
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -300,13 +292,224 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
# Get percent of profit with a custom rate (Lower than open rate)
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit_percent(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit_percent() == 0.06201057
# Test with a custom fee rate on the close trade
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
def test_clean_dry_run_db(default_conf, fee):
init(default_conf)
# Simulate dry_run entries
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='dry_run_buy_12345'
)
Trade.session.add(trade)
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='dry_run_sell_12345'
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
open_order_id='prod_buy_12345'
)
Trade.session.add(trade)
# We have 3 entries: 2 dry_run, 1 prod
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 3
clean_dry_run_db()
# We have now only the prod
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
def test_migrate_old(mocker, default_conf, fee):
"""
Test Database migration(starting with old 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 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_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
0.00258580, {stake}, {amount},
'2017-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 == "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, 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 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_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {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)
# 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
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_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", 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

14
freqtrade/tests/test_state.py Executable file
View File

@ -0,0 +1,14 @@
"""
Unit test file for constants.py
"""
from freqtrade.state import State
def test_state_object() -> None:
"""
Test the State object has the mandatory states
:return: None
"""
assert hasattr(State, 'RUNNING')
assert hasattr(State, 'STOPPED')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,29 +0,0 @@
#!/usr/bin/env python3
"""This script generate json data from bittrex"""
import json
from os import path
from freqtrade import exchange
from freqtrade.exchange import Bittrex
PAIRS = [
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
]
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
OUTPUT_DIR = path.dirname(path.realpath(__file__))
# Init Bittrex exchange
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS:
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
pair,
TICKER_INTERVAL,
))
with open(filename, 'w') as fp:
json.dump(data, fp)

Some files were not shown because too many files have changed in this diff Show More