Merge branch 'develop' into order-book

This commit is contained in:
Matthias 2018-08-29 19:32:44 +02:00
commit 9f8e68ce02
40 changed files with 775 additions and 234 deletions

View File

@ -1,10 +1,11 @@
FROM python:3.6.6-slim-stretch FROM python:3.7.0-slim-stretch
# Install TA-lib # Install TA-lib
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \ tar xzvf - && \
cd ta-lib && \ cd ta-lib && \
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \
./configure && make && make install && \ ./configure && make && make install && \
cd .. && rm -rf ta-lib cd .. && rm -rf ta-lib
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib

View File

@ -24,7 +24,7 @@ hesitate to read the source code and understand the mechanism of this bot.
## Exchange marketplaces supported ## Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Features ## Features
@ -152,6 +152,13 @@ The project is currently setup in two main branches:
- `develop` - This branch has often new features, but might also cause breaking changes. - `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. - `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
- `feat/*` - These are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature.
## A note on Binance
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
## Support ## Support

View File

@ -52,7 +52,8 @@
], ],
"pair_blacklist": [ "pair_blacklist": [
"DOGE/BTC" "DOGE/BTC"
] ],
"outdated_offset": 5
}, },
"experimental": { "experimental": {
"use_sell_signal": false, "use_sell_signal": false,

View File

@ -151,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance
Then run: Then run:
```bash ```bash
python scripts/download_backtest_data --exchange binance python scripts/download_backtest_data.py --exchange binance
``` ```
This will download ticker data for all the currency pairs you defined in `pairs.json`. This will download ticker data for all the currency pairs you defined in `pairs.json`.
@ -238,6 +238,31 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
profit. Hence, keep in mind that your performance is a mix of your profit. Hence, keep in mind that your performance is a mix of your
strategies, your configuration, and the crypto-currency you have set up. strategies, your configuration, and the crypto-currency you have set up.
## Backtesting multiple strategies
To backtest multiple strategies, a list of Strategies can be provided.
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
strategies you'd like to compare, this should give a nice runtime boost.
All listed Strategies need to be in the same folder.
``` bash
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
```
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
Detailed output for all strategies one after the other will be available, so make sure to scroll up.
```
=================================================== Strategy Summary ====================================================
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss |
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:|
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 |
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 |
```
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the Great, your strategy is profitable. What if the bot can give your the

View File

@ -1,13 +1,15 @@
# Bot usage # Bot usage
This page explains the difference parameters of the bot and how to run
it. This page explains the difference parameters of the bot and how to run it.
## Table of Contents ## Table of Contents
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands) - [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands) - [Hyperopt commands](#hyperopt-commands)
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]] [--strategy-path PATH] [--dynamic-whitelist [INT]]
@ -41,6 +43,7 @@ optional arguments:
``` ```
### How to use a different config file? ### How to use a different config file?
The bot allows you to select which config file you want to use. Per The bot allows you to select which config file you want to use. Per
default, the bot will load the file `./config.json` default, the bot will load the file `./config.json`
@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json
``` ```
### How to use --strategy? ### How to use --strategy?
This parameter will allow you to load your custom strategy class. This parameter will allow you to load your custom strategy class.
Per default without `--strategy` or `-s` the bot will load the Per default without `--strategy` or `-s` the bot will load the
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). `DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this
**Example:** **Example:**
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it: a strategy class called `AwesomeStrategy` to load it:
```bash ```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy python3 ./freqtrade/main.py --strategy AwesomeStrategy
``` ```
@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
### How to use --strategy-path? ### How to use --strategy-path?
This parameter allows you to add an additional strategy lookup path, which gets This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!): checked before the default locations (The passed path must be a folder!):
```bash ```bash
@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
``` ```
#### How to install a strategy? #### How to install a strategy?
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the folder
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
### How to use --dynamic-whitelist? ### How to use --dynamic-whitelist?
Per default `--dynamic-whitelist` will retrieve the 20 currencies based Per default `--dynamic-whitelist` will retrieve the 20 currencies based
on BaseVolume. This value can be changed when you run the script. on BaseVolume. This value can be changed when you run the script.
**By Default** **By Default**
Get the 20 currencies based on BaseVolume. Get the 20 currencies based on BaseVolume.
```bash ```bash
python3 ./freqtrade/main.py --dynamic-whitelist python3 ./freqtrade/main.py --dynamic-whitelist
``` ```
**Customize the number of currencies to retrieve** **Customize the number of currencies to retrieve**
Get the 30 currencies based on BaseVolume. Get the 30 currencies based on BaseVolume.
```bash ```bash
python3 ./freqtrade/main.py --dynamic-whitelist 30 python3 ./freqtrade/main.py --dynamic-whitelist 30
``` ```
@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20). value (20).
### How to use --db-url? ### How to use --db-url?
When you run the bot in Dry-run mode, per default no transactions are When you run the bot in Dry-run mode, per default no transactions are
stored in a database. If you want to store your bot actions in a DB stored in a database. If you want to store your bot actions in a DB
using `--db-url`. This can also be used to specify a custom database using `--db-url`. This can also be used to specify a custom database
@ -111,14 +122,14 @@ in production mode. Example command:
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
Backtesting also uses the config specified via `-c/--config`. Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
[--timerange TIMERANGE] [-l] [-r] [--timerange TIMERANGE] [-l] [-r]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH] [--export EXPORT] [--export-filename PATH]
optional arguments: optional arguments:
@ -139,6 +150,13 @@ optional arguments:
refresh the pairs files in tests/testdata with the refresh the pairs files in tests/testdata with the
latest data from the exchange. Use it if you want to latest data from the exchange. Use it if you want to
run your backtesting with up-to-date data. run your backtesting with up-to-date data.
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a commaseparated list of strategies to
backtest Please note that ticker-interval needs to be
set either in config or via command line. When using
this together with --export trades, the strategy-name
is injected into the filename (so backtest-data.json
becomes backtest-data-DefaultStrategy.json
--export EXPORT export backtest results, argument are: trades Example --export EXPORT export backtest results, argument are: trades Example
--export=trades --export=trades
--export-filename PATH --export-filename PATH
@ -151,6 +169,7 @@ optional arguments:
``` ```
### How to use --refresh-pairs-cached parameter? ### How to use --refresh-pairs-cached parameter?
The first time your run Backtesting, it will take the pairs you have The first time your run Backtesting, it will take the pairs you have
set in your config file and download data from Bittrex. set in your config file and download data from Bittrex.
@ -162,7 +181,6 @@ to come back to the previous version.**
To test your strategy with latest data, we recommend continuing using To test your strategy with latest data, we recommend continuing using
the parameter `-l` or `--live`. the parameter `-l` or `--live`.
## Hyperopt commands ## Hyperopt commands
To optimize your strategy, you can use hyperopt parameter hyperoptimization To optimize your strategy, you can use hyperopt parameter hyperoptimization
@ -194,10 +212,11 @@ optional arguments:
``` ```
## A parameter missing in the configuration? ## A parameter missing in the configuration?
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
## Next step ## Next step
The optimal strategy of the bot will change with time depending of the
market trends. The next step is to The optimal strategy of the bot will change with time depending of the market trends. The next step is to
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).

View File

@ -267,6 +267,7 @@ Official webpage: https://mrjbq7.github.io/ta-lib/install.html
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz tar xvzf ta-lib-0.4.0-src.tar.gz
cd ta-lib cd ta-lib
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
./configure --prefix=/usr ./configure --prefix=/usr
make make
make install make install

View File

@ -1,4 +1,5 @@
# Sandbox API testing # Sandbox API testing
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these. Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
This document is a *light overview of configuring Freqtrade and GDAX sandbox. This document is a *light overview of configuring Freqtrade and GDAX sandbox.
@ -11,8 +12,11 @@ https://public.sandbox.gdax.com
https://api-public.sandbox.gdax.com https://api-public.sandbox.gdax.com
--- ---
# Configure a Sandbox account on Gdax # Configure a Sandbox account on Gdax
Aim of this document section Aim of this document section
- An sanbox account - An sanbox account
- create 2FA (needed to create an API) - create 2FA (needed to create an API)
- Add test 50BTC to account - Add test 50BTC to account
@ -30,29 +34,34 @@ After registration and Email confimation you wil be redirected into your sanbox
> https://public.sandbox.pro.coinbase.com/ > https://public.sandbox.pro.coinbase.com/
## Enable 2Fa (a prerequisite to creating sandbox API Keys) ## Enable 2Fa (a prerequisite to creating sandbox API Keys)
From within sand box site select your profile, top right. From within sand box site select your profile, top right.
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile >Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
From the menu panel to the left of the screen select From the menu panel to the left of the screen select
> Security: "*View or Update*" > Security: "*View or Update*"
In the new site select "enable authenticator" as typical google Authenticator. In the new site select "enable authenticator" as typical google Authenticator.
- open Google Authenticator on your phone - open Google Authenticator on your phone
- scan barcode - scan barcode
- enter your generated 2fa - enter your generated 2fa
## Enable API Access ## Enable API Access
From within sandbox select profile>api>create api-keys From within sandbox select profile>api>create api-keys
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api >or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2Fa Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2FA
- **Copy and paste the Passphase** into a notepade this will be needed later - **Copy and paste the Passphase** into a notepade this will be needed later
- **Copy and paste the API Secret** popup into a notepad this will needed later - **Copy and paste the API Secret** popup into a notepad this will needed later
- **Copy and paste the API Key** into a notepad this will needed later - **Copy and paste the API Key** into a notepad this will needed later
## Add 50 BTC test funds ## Add 50 BTC test funds
To add funds, use the web interface deposit and withdraw buttons.
To add funds, use the web interface deposit and withdraw buttons.
To begin select 'Wallets' from the top menu. To begin select 'Wallets' from the top menu.
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets > Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
@ -64,10 +73,13 @@ To begin select 'Wallets' from the top menu.
- - - - - Deposit - - - - - Deposit
*This process may be repeated for other currencies, ETH as example* *This process may be repeated for other currencies, ETH as example*
--- ---
# Configure Freqtrade to use Gax Sandbox # Configure Freqtrade to use Gax Sandbox
The aim of this document section The aim of this document section
- Enable sandbox URLs in Freqtrade - Enable sandbox URLs in Freqtrade
- Configure API - Configure API
- - secret - - secret
@ -75,77 +87,55 @@ The aim of this document section
- - passphrase - - passphrase
## Sandbox URLs ## Sandbox URLs
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
These include `['test']` and `['api']`. These include `['test']` and `['api']`.
- `[Test]` if available will point to an Exchanges sandbox. - `[Test]` if available will point to an Exchanges sandbox.
- `[Api]` normally used, and resolves to live API target on the exchange - `[Api]` normally used, and resolves to live API target on the exchange
To make use of sandbox / test add "sandbox": true, to your config.json To make use of sandbox / test add "sandbox": true, to your config.json
```
```json
"exchange": { "exchange": {
"name": "gdax", "name": "gdax",
"sandbox": true, "sandbox": true,
"key": "5wowfxemogxeowo;heiohgmd", "key": "5wowfxemogxeowo;heiohgmd",
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
"password": "1bkjfkhfhfu6sr", "password": "1bkjfkhfhfu6sr",
"outdated_offset": 5
"pair_whitelist": [ "pair_whitelist": [
"BTC/USD" "BTC/USD"
``` ```
Also insert your Also insert your
- api-key (noted earlier) - api-key (noted earlier)
- api-secret (noted earlier) - api-secret (noted earlier)
- password (the passphrase - noted earlier) - password (the passphrase - noted earlier)
--- ---
## You should now be ready to test your sandbox!
## You should now be ready to test your sandbox
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox.
** Typically the BTC/USD has the most activity in sandbox to test against. ** Typically the BTC/USD has the most activity in sandbox to test against.
## GDAX - Old Candles problem ## GDAX - Old Candles problem
It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks
To disable this check, edit: It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks.
>strategy/interface.py
Look for the following section:
```
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
```
You could Hash out the entire check as follows: To disable this check, add / change the `"outdated_offset"` parameter in the exchange section of your configuration to adjust for this delay.
``` Example based on the above configuration:
# # Check if dataframe is out of date
# signal_date = arrow.get(latest['date'])
# interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
# if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
# logger.warning(
# 'Outdated history for pair %s. Last tick is %s minutes old',
# pair,
# (arrow.utcnow() - signal_date).seconds // 60
# )
# return False, False
```
Or inrease the timeout to offer a level of protection/alignment of this test to freqtrade in live. ```json
"exchange": {
As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)" "name": "gdax",
``` "sandbox": true,
# Check if dataframe is out of date "key": "5wowfxemogxeowo;heiohgmd",
signal_date = arrow.get(latest['date']) "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] "password": "1bkjfkhfhfu6sr",
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))): "outdated_offset": 30
logger.warning( "pair_whitelist": [
'Outdated history for pair %s. Last tick is %s minutes old', "BTC/USD"
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
``` ```

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.17.1' __version__ = '0.17.2'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@ -142,6 +142,16 @@ class Arguments(object):
action='store_true', action='store_true',
dest='refresh_pairs', dest='refresh_pairs',
) )
parser.add_argument(
'--strategy-list',
help='Provide a commaseparated list of strategies to backtest '
'Please note that ticker-interval needs to be set either in config '
'or via command line. When using this together with --export trades, '
'the strategy-name is injected into the filename '
'(so backtest-data.json becomes backtest-data-DefaultStrategy.json',
nargs='+',
dest='strategy_list',
)
parser.add_argument( parser.add_argument(
'--export', '--export',
help='export backtest results, argument are: trades\ help='export backtest results, argument are: trades\

View File

@ -187,6 +187,14 @@ class Configuration(object):
config.update({'refresh_pairs': True}) config.update({'refresh_pairs': True})
logger.info('Parameter -r/--refresh-pairs-cached detected ...') logger.info('Parameter -r/--refresh-pairs-cached detected ...')
if 'strategy_list' in self.args and self.args.strategy_list:
config.update({'strategy_list': self.args.strategy_list})
logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list))
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
logger.info('Overriding ticker interval with Command line argument')
# If --export is used we add it to the configuration # If --export is used we add it to the configuration
if 'export' in self.args and self.args.export: if 'export' in self.args and self.args.export:
config.update({'export': self.args.export}) config.update({'export': self.args.export})

View File

@ -36,7 +36,7 @@ SUPPORTED_FIAT = [
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
] ]
# Required json-schema for user specified config # Required json-schema for user specified config
@ -45,7 +45,7 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0}, 'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': { 'stake_amount': {
"type": ["number", "string"], "type": ["number", "string"],
"minimum": 0.0005, "minimum": 0.0005,
@ -162,7 +162,8 @@ CONF_SCHEMA = {
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
}, },
'uniqueItems': True 'uniqueItems': True
} },
'outdated_offset': {'type': 'integer', 'minimum': 1}
}, },
'required': ['name', 'key', 'secret', 'pair_whitelist'] 'required': ['name', 'key', 'secret', 'pair_whitelist']
} }

View File

@ -322,7 +322,7 @@ class Exchange(object):
return data return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
else: else:
@ -330,7 +330,7 @@ class Exchange(object):
return self._cached_ticker[pair] return self._cached_ticker[pair]
@retrier @retrier
def get_ticker_history(self, pair: str, tick_interval: str, def get_candle_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> List[Dict]: since_ms: Optional[int] = None) -> List[Dict]:
try: try:
# last item should be in the time interval [now - tick_interval, now] # last item should be in the time interval [now - tick_interval, now]
@ -493,12 +493,3 @@ class Exchange(object):
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
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

@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:
""" """
Analyses the trend for the given ticker history Analyses the trend for the given ticker history
:param ticker: See exchange.get_ticker_history :param ticker: See exchange.get_candle_history
:return: DataFrame :return: DataFrame
""" """
cols = ['date', 'open', 'high', 'low', 'close', 'volume'] cols = ['date', 'open', 'high', 'low', 'close', 'volume']

View File

@ -95,6 +95,8 @@ class FreqtradeBot(object):
'status': f'{state.name.lower()}' 'status': f'{state.name.lower()}'
}) })
logger.info('Changing state to: %s', state.name) logger.info('Changing state to: %s', state.name)
if state == State.RUNNING:
self._startup_messages()
if state == State.STOPPED: if state == State.STOPPED:
time.sleep(1) time.sleep(1)
@ -111,6 +113,38 @@ class FreqtradeBot(object):
nb_assets=nb_assets) nb_assets=nb_assets)
return state return state
def _startup_messages(self) -> None:
if self.config.get('dry_run', False):
self.rpc.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'Dry run is enabled. All trades are simulated.'
})
stake_currency = self.config['stake_currency']
stake_amount = self.config['stake_amount']
minimal_roi = self.config['minimal_roi']
ticker_interval = self.config['ticker_interval']
exchange_name = self.config['exchange']['name']
strategy_name = self.config.get('strategy', '')
self.rpc.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
f'*Ticker Interval:* `{ticker_interval}`\n'
f'*Strategy:* `{strategy_name}`'
})
if self.config.get('dynamic_whitelist', False):
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
specific_pairs = ''
else:
top_pairs = 'whitelisted'
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
f'{specific_pairs}'
})
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
""" """
Throttles the given callable that it Throttles the given callable that it
@ -355,7 +389,7 @@ class FreqtradeBot(object):
# Pick pair based on buy signals # Pick pair based on buy signals
for _pair in whitelist: for _pair in whitelist:
thistory = self.exchange.get_ticker_history(_pair, interval) thistory = self.exchange.get_candle_history(_pair, interval)
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory) (buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
if buy and not sell: if buy and not sell:
@ -547,7 +581,7 @@ class FreqtradeBot(object):
(buy, sell) = (False, False) (buy, sell) = (False, False)
experimental = self.config.get('experimental', {}) experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
ticker = self.exchange.get_ticker_history(trade.pair, self.strategy.ticker_interval) ticker = self.exchange.get_candle_history(trade.pair, self.strategy.ticker_interval)
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval, (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
ticker) ticker)

View File

@ -1,7 +1,13 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import gzip import gzip
import json try:
import ujson as json
_UJSON = True
except ImportError:
# see mypy/issues/1153
import json # type: ignore
_UJSON = False
import logging import logging
import os import os
from typing import Optional, List, Dict, Tuple, Any from typing import Optional, List, Dict, Tuple, Any
@ -14,6 +20,14 @@ from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def json_load(data):
"""Try to load data with ujson"""
if _UJSON:
return json.load(data, precise_float=True)
else:
return json.load(data)
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist: if not tickerlist:
return tickerlist return tickerlist
@ -163,7 +177,7 @@ def load_cached_data_for_updating(filename: str,
# read the cached file # read the cached file
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename, "rt") as file: with open(filename, "rt") as file:
data = json.load(file) data = json_load(file)
# remove the last item, because we are not sure if it is correct # remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted # it could be fetched when the candle was incompleted
if data: if data:
@ -219,7 +233,7 @@ def download_backtesting_testdata(datadir: str,
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, new_data = exchange.get_candle_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms) since_ms=since_ms)
data.extend(new_data) data.extend(new_data)

View File

@ -6,7 +6,9 @@ This module contains the backtesting logic
import logging import logging
import operator import operator
from argparse import Namespace from argparse import Namespace
from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Tuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow import arrow
@ -52,13 +54,9 @@ class Backtesting(object):
backtesting = Backtesting(config) backtesting = Backtesting(config)
backtesting.start() backtesting.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.advise_buy = self.strategy.advise_buy
self.advise_sell = self.strategy.advise_sell
# Reset keys for backtesting # Reset keys for backtesting
self.config['exchange']['key'] = '' self.config['exchange']['key'] = ''
@ -66,9 +64,36 @@ class Backtesting(object):
self.config['exchange']['password'] = '' self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = '' self.config['exchange']['uid'] = ''
self.config['dry_run'] = True self.config['dry_run'] = True
self.strategylist: List[IStrategy] = []
if self.config.get('strategy_list', None):
# Force one interval
self.ticker_interval = str(self.config.get('ticker_interval'))
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy)
else:
# only one strategy
strat = StrategyResolver(self.config).strategy
self.strategylist.append(StrategyResolver(self.config).strategy)
# Load one strategy
self._set_strategy(self.strategylist[0])
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee() self.fee = self.exchange.get_fee()
def _set_strategy(self, strategy):
"""
Load strategy into backtesting
"""
self.strategy = strategy
self.ticker_interval = self.config.get('ticker_interval')
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
self.advise_buy = strategy.advise_buy
self.advise_sell = strategy.advise_sell
@staticmethod @staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
@ -132,7 +157,32 @@ class Backtesting(object):
tabular_data.append([reason.value, count]) tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe") return tabulate(tabular_data, headers=headers, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: def _generate_text_table_strategy(self, all_results: dict) -> str:
"""
Generate summary table per strategy
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for strategy, results in all_results.items():
tabular_data.append([
strategy,
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(), records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration, t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
@ -140,6 +190,11 @@ class Backtesting(object):
for index, t in results.iterrows()] for index, t in results.iterrows()]
if records: if records:
if strategyname:
# Inject strategyname to filename
recname = Path(recordfilename)
recordfilename = str(Path.joinpath(
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
logger.info('Dumping backtest results to %s', recordfilename) logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records) file_dump_json(recordfilename, records)
@ -283,7 +338,7 @@ class Backtesting(object):
if self.config.get('live'): if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs: for pair in pairs:
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval)
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
@ -307,7 +362,13 @@ class Backtesting(object):
else: else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0 max_open_trades = 0
all_results = {}
for strat in self.strategylist:
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
self._set_strategy(strat)
# need to reprocess data every time to populate signals
preprocessed = self.tickerdata_to_dataframe(data) preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe # Print timeframe
@ -320,7 +381,7 @@ class Backtesting(object):
) )
# Execute backtest and print results # Execute backtest and print results
results = self.backtest( all_results[self.strategy.get_strategy_name()] = self.backtest(
{ {
'stake_amount': self.config.get('stake_amount'), 'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed, 'processed': preprocessed,
@ -329,40 +390,27 @@ class Backtesting(object):
} }
) )
for strategy, results in all_results.items():
if self.config.get('export', False): if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results) self._store_backtest_result(self.config['exportfilename'], results,
strategy if len(self.strategylist) > 1 else None)
logger.info( print(f"Result for strategy {strategy}")
'\n' + '=' * 49 + print(' BACKTESTING REPORT '.center(119, '='))
' BACKTESTING REPORT ' + print(self._generate_text_table(data, results))
'=' * 50 + '\n'
'%s',
self._generate_text_table(
data,
results
)
)
# logger.info(
# results[['sell_reason']].groupby('sell_reason').count()
# )
logger.info( print(' SELL REASON STATS '.center(119, '='))
'\n' + print(self._generate_text_table_sell_reason(data, results))
' SELL READON STATS '.center(119, '=') +
'\n%s \n',
self._generate_text_table_sell_reason(data, results)
) print(' LEFT OPEN TRADES REPORT '.center(119, '='))
print(self._generate_text_table(data, results.loc[results.open_at_end]))
logger.info( print()
'\n' + if len(all_results) > 1:
' LEFT OPEN TRADES REPORT '.center(119, '=') + # Print Strategy summary table
'\n%s', print(' Strategy Summary '.center(119, '='))
self._generate_text_table( print(self._generate_text_table_strategy(all_results))
data, print('\nFor more details, please look at the detail tables above')
results.loc[results.open_at_end]
)
)
def setup_configuration(args: Namespace) -> Dict[str, Any]: def setup_configuration(args: Namespace) -> Dict[str, Any]:

View File

@ -79,10 +79,12 @@ def check_migrate(engine) -> None:
table_back_name = 'trades_bak' table_back_name = 'trades_bak'
for i, table_back_name in enumerate(tabs): for i, table_back_name in enumerate(tabs):
table_back_name = f'trades_bak{i}' table_back_name = f'trades_bak{i}'
logger.info(f'trying {table_back_name}') logger.debug(f'trying {table_back_name}')
# Check for latest column # Check for latest column
if not has_column(cols, 'max_rate'): if not has_column(cols, 'ticker_interval'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_close = get_column_def(cols, 'fee_close', 'fee') fee_close = get_column_def(cols, 'fee_close', 'fee')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
@ -157,8 +159,8 @@ class Trade(_DECL_BASE):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float) open_rate = Column(Float)

View File

@ -13,6 +13,7 @@ import sqlalchemy as sql
from numpy import mean, nan_to_num from numpy import mean, nan_to_num
from pandas import DataFrame from pandas import DataFrame
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -24,6 +25,8 @@ logger = logging.getLogger(__name__)
class RPCMessageType(Enum): class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status' STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom'
BUY_NOTIFICATION = 'buy' BUY_NOTIFICATION = 'buy'
SELL_NOTIFICATION = 'sell' SELL_NOTIFICATION = 'sell'
@ -271,10 +274,13 @@ class RPC(object):
if coin == 'BTC': if coin == 'BTC':
rate = 1.0 rate = 1.0
else: else:
try:
if coin == 'USDT': if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid'] rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
else: else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
except TemporaryError:
continue
est_btc: float = rate * balance['total'] est_btc: float = rate * balance['total']
total = total + est_btc total = total + est_btc
output.append({ output.append({

View File

@ -154,6 +154,12 @@ class Telegram(RPC):
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg) message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '*Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
message = '{status}'.format(**msg)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@ -1,4 +1,5 @@
import logging import logging
import sys
from copy import deepcopy from copy import deepcopy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@ -12,8 +13,18 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
Imports given Strategy instance to global scope Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it of freqtrade.strategy and returns an instance of it
""" """
# Copy all attributes from base class and class # Copy all attributes from base class and class
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
# `TypeError: can't pickle _abc_data objects``
# This will only apply to python 3.7
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
del comb['_abc_impl']
attr = deepcopy(comb)
# Adjust module name # Adjust module name
attr['__module__'] = 'freqtrade.strategy' attr['__module__'] = 'freqtrade.strategy'

View File

@ -155,7 +155,8 @@ class IStrategy(ABC):
# Check if dataframe is out of date # Check if dataframe is out of date
signal_date = arrow.get(latest['date']) signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
logger.warning( logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old', 'Outdated history for pair %s. Last tick is %s minutes old',
pair, pair,

View File

@ -44,14 +44,15 @@ class StrategyResolver(object):
# Check if we need to override configuration # Check if we need to override configuration
if 'minimal_roi' in config: if 'minimal_roi' in config:
self.strategy.minimal_roi = config['minimal_roi'] self.strategy.minimal_roi = config['minimal_roi']
logger.info("Override strategy \'minimal_roi\' with value in config file.") logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
config['minimal_roi'])
else: else:
config['minimal_roi'] = self.strategy.minimal_roi config['minimal_roi'] = self.strategy.minimal_roi
if 'stoploss' in config: if 'stoploss' in config:
self.strategy.stoploss = config['stoploss'] self.strategy.stoploss = config['stoploss']
logger.info( logger.info(
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss'] "Override strategy 'stoploss' with value in config file: %s.", config['stoploss']
) )
else: else:
config['stoploss'] = self.strategy.stoploss config['stoploss'] = self.strategy.stoploss
@ -59,7 +60,7 @@ class StrategyResolver(object):
if 'ticker_interval' in config: if 'ticker_interval' in config:
self.strategy.ticker_interval = config['ticker_interval'] self.strategy.ticker_interval = config['ticker_interval']
logger.info( logger.info(
"Override strategy \'ticker_interval\' with value in config file: %s.", "Override strategy 'ticker_interval' with value in config file: %s.",
config['ticker_interval'] config['ticker_interval']
) )
else: else:

View File

@ -553,7 +553,7 @@ def make_fetch_ohlcv_mock(data):
return fetch_ohlcv_mock return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker): def test_get_candle_history(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
tick = [ tick = [
[ [
@ -570,7 +570,7 @@ def test_get_ticker_history(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker # retrieve original ticker
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000 assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1 assert ticks[0][1] == 1
assert ticks[0][2] == 2 assert ticks[0][2] == 2
@ -592,7 +592,7 @@ def test_get_ticker_history(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000 assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6 assert ticks[0][1] == 6
assert ticks[0][2] == 7 assert ticks[0][2] == 7
@ -601,16 +601,16 @@ def test_get_ticker_history(default_conf, mocker):
assert ticks[0][5] == 10 assert ticks[0][5] == 10
ccxt_exceptionhandlers(mocker, default_conf, api_mock, ccxt_exceptionhandlers(mocker, default_conf, api_mock,
"get_ticker_history", "fetch_ohlcv", "get_candle_history", "fetch_ohlcv",
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker): def test_get_candle_history_sort(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
# GDAX use-case (real data from GDAX) # GDAX use-case (real data from GDAX)
@ -633,7 +633,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000 assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649 assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651 assert ticks[0][2] == 0.07651
@ -666,7 +666,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000 assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999 assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766 assert ticks[0][2] == 0.0766
@ -852,15 +852,3 @@ def test_get_fee(default_conf, mocker):
ccxt_exceptionhandlers(mocker, default_conf, api_mock, ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_fee', 'calculate_fee') '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

@ -110,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
return pairdata return pairdata
# use for mock freqtrade.exchange.get_ticker_history' # use for mock freqtrade.exchange.get_candle_history'
def _load_pair_as_ticks(pair, tickfreq): def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair]) ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -201) ticks = trim_dictlist(ticks, -201)
@ -406,12 +406,56 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
data={'ETH/BTC': {}}, results=results) == result_str data={'ETH/BTC': {}}, results=results) == result_str
def test_generate_text_table_strategyn(default_conf, mocker):
"""
Test Backtesting.generate_text_table_sell_reason() method
"""
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
results = {}
results['ETH/BTC'] = pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, 0.3],
'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10],
'profit': [2, 0, 0],
'loss': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
results['LTC/BTC'] = pd.DataFrame(
{
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
'profit_percent': [0.4, 0.2, 0.3],
'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15],
'profit': [4, 1, 0],
'loss': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
result_str = (
'| Strategy | buy count | avg profit % | cum profit % '
'| total profit BTC | avg duration | profit | loss |\n'
'|:-----------|------------:|---------------:|---------------:'
'|-------------------:|:---------------|---------:|-------:|\n'
'| ETH/BTC | 3 | 20.00 | 60.00 '
'| 1.10000000 | 0:17:00 | 3 | 0 |\n'
'| LTC/BTC | 3 | 30.00 | 90.00 '
'| 1.30000000 | 0:20:00 | 3 | 0 |'
)
print(backtesting._generate_text_table_strategy(all_results=results))
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None: def test_backtesting_start(default_conf, mocker, caplog) -> None:
def get_timeframe(input1, input2): def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.optimize.load_data', mocked_load_data) mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@ -446,7 +490,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@ -654,6 +698,18 @@ def test_backtest_record(default_conf, fee, mocker):
records = records[0] records = records[0]
# Ensure records are of correct type # Ensure records are of correct type
assert len(records) == 4 assert len(records) == 4
# reset test to test with strategy name
names = []
records = []
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat")
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result-DefStrat.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records # Below follows just a typecheck of the schema/type of trade-records
oix = None oix = None
@ -677,7 +733,7 @@ def test_backtest_record(default_conf, fee, mocker):
def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_live(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', mocker.patch('freqtrade.exchange.Exchange.get_candle_history',
new=lambda s, n, i: _load_pair_as_ticks(n, i)) new=lambda s, n, i: _load_pair_as_ticks(n, i))
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
@ -686,15 +742,6 @@ def test_backtest_start_live(default_conf, mocker, caplog):
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = MagicMock()
args.ticker_interval = 1
args.level = 10
args.live = True
args.datadir = None
args.export = None
args.strategy = 'DefaultStrategy'
args.timerange = '-100' # needed due to MagicMock malleability
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@ -725,3 +772,60 @@ def test_backtest_start_live(default_conf, mocker, caplog):
for line in exists: for line in exists:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog.record_tuples)
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_candle_history',
new=lambda s, n, i: _load_pair_as_ticks(n, i))
patch_exchange(mocker)
backtestmock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
gen_table_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
gen_strattable_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--datadir', 'freqtrade/tests/testdata',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100',
'--enable-position-stacking',
'--disable-max-market-positions',
'--strategy-list',
'DefaultStrategy',
'TestStrategy',
]
args = get_args(args)
start(args)
# 2 backtests, 4 tables
assert backtestmock.call_count == 2
assert gen_table_mock.call_count == 4
assert gen_strattable_mock.call_count == 1
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...',
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategy',
]
for line in exists:
assert log_has(line, caplog.record_tuples)

View File

@ -53,7 +53,7 @@ def _clean_test_file(file: str) -> None:
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None: def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
@ -63,7 +63,7 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) ->
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None: def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@ -74,7 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) ->
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
@ -87,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co
""" """
Test load_data() with 1 min ticker Test load_data() with 1 min ticker
""" """
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
@ -118,7 +118,7 @@ def test_testdata_path() -> None:
def test_download_pairs(ticker_history, mocker, default_conf) -> None: def test_download_pairs(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
@ -261,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None: def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error')) side_effect=BaseException('File Error'))
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -279,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf)
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None: def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
# Download a 1 min ticker file # Download a 1 min ticker file
@ -304,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None:
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
] ]
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')

View File

@ -6,6 +6,7 @@ from unittest.mock import MagicMock, ANY
import pytest import pytest
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -285,11 +286,12 @@ def test_rpc_balance_handle(default_conf, mocker):
'used': 2.0, 'used': 2.0,
}, },
'ETH': { 'ETH': {
'free': 0.0, 'free': 1.0,
'total': 0.0, 'total': 5.0,
'used': 0.0, 'used': 4.0,
} }
} }
# ETH will be skipped due to mocked Error below
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.fiat_convert.Market', 'freqtrade.fiat_convert.Market',
@ -301,7 +303,8 @@ def test_rpc_balance_handle(default_conf, mocker):
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=mock_balance) get_balances=MagicMock(return_value=mock_balance),
get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -320,6 +323,7 @@ def test_rpc_balance_handle(default_conf, mocker):
'pending': 2.0, 'pending': 2.0,
'est_btc': 12.0, 'est_btc': 12.0,
}] }]
assert result['total'] == 12.0
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:

View File

@ -1095,6 +1095,38 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] == '*Status:* `running`' assert msg_mock.call_args[0][0] == '*Status:* `running`'
def test_warning_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'message'
})
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
def test_custom_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': '*Custom:* `Hello World`'
})
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
def test_send_msg_unknown_type(default_conf, mocker) -> None: def test_send_msg_unknown_type(default_conf, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(

View File

@ -8,6 +8,7 @@ from pandas import DataFrame
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.persistence import Trade
from freqtrade.tests.conftest import get_patched_exchange, log_has from freqtrade.tests.conftest import get_patched_exchange, log_has
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@ -88,7 +89,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
def test_get_signal_handles_exceptions(mocker, default_conf): def test_get_signal_handles_exceptions(mocker, default_conf):
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=MagicMock())
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, 'analyze_ticker',
@ -105,3 +106,26 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
data = strategy.tickerdata_to_dataframe(tickerlist) data = strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
def test_min_roi_reached(default_conf, fee) -> None:
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)

View File

@ -130,7 +130,7 @@ def test_strategy_override_minimal_roi(caplog):
assert resolver.strategy.minimal_roi[0] == 0.5 assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'minimal_roi\' with value in config file.' "Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
) in caplog.record_tuples ) in caplog.record_tuples
@ -145,7 +145,7 @@ def test_strategy_override_stoploss(caplog):
assert resolver.strategy.stoploss == -0.5 assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'stoploss\' with value in config file: -0.5.' "Override strategy 'stoploss' with value in config file: -0.5."
) in caplog.record_tuples ) in caplog.record_tuples
@ -161,7 +161,7 @@ def test_strategy_override_ticker_interval(caplog):
assert resolver.strategy.ticker_interval == 60 assert resolver.strategy.ticker_interval == 60
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.' "Override strategy 'ticker_interval' with value in config file: 60."
) in caplog.record_tuples ) in caplog.record_tuples

View File

@ -132,7 +132,11 @@ def test_parse_args_backtesting_custom() -> None:
'backtesting', 'backtesting',
'--live', '--live',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--refresh-pairs-cached'] '--refresh-pairs-cached',
'--strategy-list',
'DefaultStrategy',
'TestStrategy'
]
call_args = Arguments(args, '').get_parsed_arg() call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json' assert call_args.config == 'test_conf.json'
assert call_args.live is True assert call_args.live is True
@ -141,6 +145,8 @@ def test_parse_args_backtesting_custom() -> None:
assert call_args.func is not None assert call_args.func is not None
assert call_args.ticker_interval == '1m' assert call_args.ticker_interval == '1m'
assert call_args.refresh_pairs is True assert call_args.refresh_pairs is True
assert type(call_args.strategy_list) is list
assert len(call_args.strategy_list) == 2
def test_parse_args_hyperopt_custom() -> None: def test_parse_args_hyperopt_custom() -> None:

View File

@ -292,6 +292,61 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
) )
def test_setup_configuration_with_stratlist(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',
'backtesting',
'--ticker-interval', '1m',
'--export', '/bar/foo',
'--strategy-list',
'DefaultStrategy',
'TestStrategy'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
assert 'strategy_list' in config
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
assert 'position_stacking' not in config
assert 'use_max_market_positions' not in config
assert 'timerange' not in config
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)

View File

@ -14,7 +14,7 @@ def load_dataframe_pair(pairs, strategy):
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
dataframe = strategy.analyze_ticker(dataframe, pairs[0]) dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]})
return dataframe return dataframe

View File

@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
:return: None :return: None
""" """
freqtrade.strategy.get_signal = lambda e, s, t: value freqtrade.strategy.get_signal = lambda e, s, t: value
freqtrade.exchange.get_ticker_history = lambda p, i: None freqtrade.exchange.get_candle_history = lambda p, i: None
def patch_RPCManager(mocker) -> MagicMock: def patch_RPCManager(mocker) -> MagicMock:
@ -553,7 +553,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker_history=MagicMock(return_value=20), get_candle_history=MagicMock(return_value=20),
get_balance=MagicMock(return_value=20), get_balance=MagicMock(return_value=20),
get_fee=fee, get_fee=fee,
) )
@ -2070,3 +2070,9 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
patch_get_signal(freqtrade, value=(False, True)) patch_get_signal(freqtrade, value=(False, True))
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
def test_startup_messages(default_conf, mocker):
default_conf['dynamic_whitelist'] = 20
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.state is State.RUNNING

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
from unittest.mock import MagicMock from unittest.mock import MagicMock
import logging
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
@ -403,7 +404,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
caplog.set_level(logging.DEBUG)
amount = 103.223 amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL, id INTEGER NOT NULL,
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
@ -418,14 +421,21 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
open_date DATETIME NOT NULL, open_date DATETIME NOT NULL,
close_date DATETIME, close_date DATETIME,
open_order_id VARCHAR, open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (is_open IN (0, 1)) CHECK (is_open IN (0, 1))
);""" );"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date) open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate)
VALUES ('binance', 'ETC/BTC', 1, {fee}, VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount}, 0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000') '2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0)
""".format(fee=fee.return_value, """.format(fee=fee.return_value,
stake=default_conf.get("stake_amount"), stake=default_conf.get("stake_amount"),
amount=amount amount=amount
@ -463,12 +473,15 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.ticker_interval is None assert trade.ticker_interval is None
assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2",
caplog.record_tuples)
def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_migrate_mid_state(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
caplog.set_level(logging.DEBUG)
amount = 103.223 amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL, id INTEGER NOT NULL,
@ -522,6 +535,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.stop_loss == 0.0 assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak0", caplog.record_tuples) assert log_has("trying trades_bak0", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak0",
caplog.record_tuples)
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):

View File

@ -0,0 +1,16 @@
import talib.abstract as ta
import pandas as pd
def test_talib_bollingerbands_near_zero_values():
inputs = pd.DataFrame([
{'close': 0.00000010},
{'close': 0.00000011},
{'close': 0.00000012},
{'close': 0.00000013},
{'close': 0.00000014}
])
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
assert (bollinger['upperband'][3] != bollinger['middleband'][3])

View File

@ -1,6 +1,6 @@
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib && ./configure && make && sudo make install && cd .. cd ta-lib && sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
else else
echo "TA-lib already installed, skipping download and build." echo "TA-lib already installed, skipping download and build."
cd ta-lib && sudo make install && cd .. cd ta-lib && sudo make install && cd ..

View File

@ -1,18 +1,18 @@
ccxt==1.17.60 ccxt==1.17.170
SQLAlchemy==1.2.10 SQLAlchemy==1.2.11
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
requests==2.19.1 requests==2.19.1
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.23.3 pandas==0.23.4
scikit-learn==0.19.2 scikit-learn==0.19.2
scipy==1.1.0 scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.15.0 numpy==1.15.1
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.7.0 pytest==3.7.2
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-cov==2.5.1 pytest-cov==2.5.1
tabulate==0.8.2 tabulate==0.8.2
@ -22,4 +22,4 @@ coinmarketcap==5.0.3
scikit-optimize==0.5.2 scikit-optimize==0.5.2
# Required for plotting data # Required for plotting data
#plotly==3.0.0 #plotly==3.1.1

View File

@ -0,0 +1,93 @@
import os
import sys
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(root + '/python')
import ccxt # noqa: E402
def style(s, style):
return style + s + '\033[0m'
def green(s):
return style(s, '\033[92m')
def blue(s):
return style(s, '\033[94m')
def yellow(s):
return style(s, '\033[93m')
def red(s):
return style(s, '\033[91m')
def pink(s):
return style(s, '\033[95m')
def bold(s):
return style(s, '\033[1m')
def underline(s):
return style(s, '\033[4m')
def dump(*args):
print(' '.join([str(arg) for arg in args]))
def print_supported_exchanges():
dump('Supported exchanges:', green(', '.join(ccxt.exchanges)))
try:
id = sys.argv[1] # get exchange id from command line arguments
# check if the exchange is supported by ccxt
exchange_found = id in ccxt.exchanges
if exchange_found:
dump('Instantiating', green(id), 'exchange')
# instantiate the exchange by id
exchange = getattr(ccxt, id)({
# 'proxy':'https://cors-anywhere.herokuapp.com/',
})
# load all markets from the exchange
markets = exchange.load_markets()
# output a list of all market symbols
dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols)
tuples = list(ccxt.Exchange.keysort(markets).items())
# debug
for (k, v) in tuples:
print(v)
# output a table of all markets
dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote')))
for (k, v) in tuples:
dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote']))
else:
dump('Exchange ' + red(id) + ' not found')
print_supported_exchanges()
except Exception as e:
dump('[' + type(e).__name__ + ']', str(e))
dump("Usage: python " + sys.argv[0], green('id'))
print_supported_exchanges()

View File

@ -138,7 +138,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
tickers = {} tickers = {}
if args.live: if args.live:
logger.info('Downloading pair.') logger.info('Downloading pair.')
tickers[pair] = exchange.get_ticker_history(pair, tick_interval) tickers[pair] = exchange.get_candle_history(pair, tick_interval)
else: else:
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=_CONF.get("datadir"), datadir=_CONF.get("datadir"),

View File

@ -1,13 +1,31 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#encoding=utf8 #encoding=utf8
# Check which python version is installed
function check_installed_python() {
which python3.7
if [ $? -eq 0 ]; then
echo "using Python 3.7"
PYTHON=python3.7
return
fi
which python3.6
if [ $? -eq 0 ]; then
echo "using Python 3.6"
PYTHON=python3.6
return
fi
}
function updateenv () { function updateenv () {
echo "-------------------------" echo "-------------------------"
echo "Update your virtual env" echo "Update your virtual env"
echo "-------------------------" echo "-------------------------"
source .env/bin/activate source .env/bin/activate
echo "pip3 install in-progress. Please wait..." echo "pip3 install in-progress. Please wait..."
pip3.6 install --quiet --upgrade pip pip3 install --quiet --upgrade pip
pip3 install --quiet -r requirements.txt --upgrade pip3 install --quiet -r requirements.txt --upgrade
pip3 install --quiet -r requirements.txt pip3 install --quiet -r requirements.txt
pip3 install --quiet -e . pip3 install --quiet -e .
@ -79,7 +97,7 @@ function reset () {
fi fi
echo echo
python3.6 -m venv .env ${PYTHON} -m venv .env
updateenv updateenv
} }
@ -183,7 +201,7 @@ function install () {
install_debian install_debian
else else
echo "This script does not support your OS." echo "This script does not support your OS."
echo "If you have Python3.6, pip, virtualenv, ta-lib you can continue." echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10 sleep 10
fi fi
@ -193,7 +211,7 @@ function install () {
echo "-------------------------" echo "-------------------------"
echo "Run the bot" echo "Run the bot"
echo "-------------------------" echo "-------------------------"
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'." echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
} }
function plot () { function plot () {
@ -214,6 +232,9 @@ function help () {
echo " -p,--plot Install dependencies for Plotting scripts." echo " -p,--plot Install dependencies for Plotting scripts."
} }
# Verify if 3.6 or 3.7 is installed
check_installed_python
case $* in case $* in
--install|-i) --install|-i)
install install