This commit is contained in:
Matthias 2018-08-14 12:35:29 +00:00 committed by GitHub
commit f67ee6171d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 866 additions and 526 deletions

View File

@ -5,6 +5,7 @@ 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

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

@ -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\
@ -161,14 +171,6 @@ class Arguments(object):
dest='exportfilename', dest='exportfilename',
metavar='PATH', metavar='PATH',
) )
parser.add_argument(
'--backslap',
help="Utilize the Backslapping approach instead of the default Backtesting. This should provide more "
"accurate results, unless you are utilizing Min/Max function in your strategy.",
required=False,
dest='backslap',
action='store_true'
)
@staticmethod @staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None: def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
@ -236,7 +238,7 @@ class Arguments(object):
Builds and attaches all subcommands Builds and attaches all subcommands
:return: None :return: None
""" """
from freqtrade.optimize import backtesting, hyperopt from freqtrade.optimize import backtesting, backslapping, hyperopt
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='subparser')
@ -246,6 +248,12 @@ class Arguments(object):
self.optimizer_shared_options(backtesting_cmd) self.optimizer_shared_options(backtesting_cmd)
self.backtesting_options(backtesting_cmd) self.backtesting_options(backtesting_cmd)
# Add backslapping subcommand
backslapping_cmd = subparsers.add_parser('backslapping', help='backslapping module')
backslapping_cmd.set_defaults(func=backslapping.start)
self.optimizer_shared_options(backslapping_cmd)
self.backtesting_options(backslapping_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start) hyperopt_cmd.set_defaults(func=hyperopt.start)

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,

View File

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

View File

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

@ -330,7 +330,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:
@ -497,7 +497,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

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

@ -1,48 +1,36 @@
import timeit import timeit
from argparse import Namespace
import logging
from typing import Dict, Any from typing import Dict, Any
from pandas import DataFrame from pandas import DataFrame
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize.optimize import IOptimize, BacktestResult, OptimizeType, setup_configuration
from freqtrade.strategy import IStrategy from freqtrade.strategy import IStrategy
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import StrategyResolver from freqtrade.strategy.resolver import StrategyResolver
logger = logging.getLogger(__name__)
class Backslapping:
class Backslapping(IOptimize):
""" """
provides a quick way to evaluate strategies over a longer term of time provides a quick way to evaluate strategies over a longer term of time
""" """
def __init__(self, config: Dict[str, Any], exchange = None) -> None: def __init__(self, config: Dict[str, Any]) -> None:
""" """
constructor constructor
""" """
super().__init__(config)
self.config = config self._optimizetype = OptimizeType.BACKTEST
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.populate_buy_trend = self.strategy.populate_buy_trend
self.populate_sell_trend = self.strategy.populate_sell_trend
###
#
###
if exchange is None:
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.exchange = Exchange(self.config)
else:
self.exchange = exchange
self.fee = self.exchange.get_fee() self.fee = self.exchange.get_fee()
self.stop_loss_value = self.strategy.stoploss self.stop_loss_value = self.strategy.stoploss
#### backslap config # backslap config
''' '''
Numpy arrays are used for 100x speed up Numpy arrays are used for 100x speed up
We requires setting Int values for We requires setting Int values for
@ -81,7 +69,7 @@ class Backslapping:
def f(self, st): def f(self, st):
return (timeit.default_timer() - st) return (timeit.default_timer() - st)
def run(self,args): def run(self, args):
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
processed = args['processed'] processed = args['processed']
@ -96,8 +84,8 @@ class Backslapping:
if self.debug_timing: # Start timer if self.debug_timing: # Start timer
fl = self.s() fl = self.s()
ticker_data = self.populate_sell_trend( ticker_data = self.advise_sell(self.advise_buy(pair_data, {'pair': pair}),
self.populate_buy_trend(pair_data))[headers].copy() {'pair': pair})[headers].copy()
if self.debug_timing: # print time taken if self.debug_timing: # print time taken
flt = self.f(fl) flt = self.f(fl)
@ -132,7 +120,7 @@ class Backslapping:
bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair) bslap_results_df = self.vector_fill_results_table(bslap_results_df, pair)
else: else:
from freqtrade.optimize.backtesting import BacktestResult
bslap_results_df = [] bslap_results_df = []
bslap_results_df = DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields) bslap_results_df = DataFrame.from_records(bslap_results_df, columns=BacktestResult._fields)
@ -221,13 +209,13 @@ class Backslapping:
""" """
The purpose of this def is to return the next "buy" = 1 The purpose of this def is to return the next "buy" = 1
after t_exit_ind. after t_exit_ind.
This function will also check is the stop limit for the pair has been reached. This function will also check is the stop limit for the pair has been reached.
if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit. if stop_stops is the limit and stop_stops_count it the number of times the stop has been hit.
t_exit_ind is the index the last trade exited on t_exit_ind is the index the last trade exited on
or 0 if first time around this loop. or 0 if first time around this loop.
stop_stops i stop_stops i
""" """
debug = self.debug debug = self.debug
@ -379,7 +367,7 @@ class Backslapping:
a) Find first buy index a) Find first buy index
b) Discover first stop and sell hit after buy index b) Discover first stop and sell hit after buy index
c) Chose first instance as trade exit c) Chose first instance as trade exit
Phase 2 Phase 2
2) Manage dynamic Stop and ROI Exit 2) Manage dynamic Stop and ROI Exit
a) Create trade slice from 1 a) Create trade slice from 1
@ -392,14 +380,14 @@ class Backslapping:
''' '''
0 - Find next buy entry 0 - Find next buy entry
Finds index for first (buy = 1) flag Finds index for first (buy = 1) flag
Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1" Requires: np_buy_arr - a 1D array of the 'buy' column. To find next "1"
Required: t_exit_ind - Either 0, first loop. Or The index we last exited on Required: t_exit_ind - Either 0, first loop. Or The index we last exited on
Requires: np_buy_arr_len - length of pair array. Requires: np_buy_arr_len - length of pair array.
Requires: stops_stops - number of stops allowed before stop trading a pair Requires: stops_stops - number of stops allowed before stop trading a pair
Requires: stop_stop_counts - count of stops hit in the pair Requires: stop_stop_counts - count of stops hit in the pair
Provides: The next "buy" index after t_exit_ind Provides: The next "buy" index after t_exit_ind
If -1 is returned no buy has been found in remainder of array, skip to exit loop If -1 is returned no buy has been found in remainder of array, skip to exit loop
''' '''
t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count) t_open_ind = self.np_get_t_open_ind(np_buy_arr, t_exit_ind, np_buy_arr_len, stop_stops, stop_stops_count)
@ -416,19 +404,19 @@ class Backslapping:
""" """
1 - Create views to search within for our open trade 1 - Create views to search within for our open trade
The views are our search space for the next Stop or Sell The views are our search space for the next Stop or Sell
Numpy view is employed as: Numpy view is employed as:
1,000 faster than pandas searches 1,000 faster than pandas searches
Pandas cannot assure it will always return a view, it may make a slow copy. Pandas cannot assure it will always return a view, it may make a slow copy.
The view contains columns: The view contains columns:
buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5
Requires: np_bslap is our numpy array of the ticker DataFrame Requires: np_bslap is our numpy array of the ticker DataFrame
Requires: t_open_ind is the index row with the buy. Requires: t_open_ind is the index row with the buy.
Provides: np_t_open_v View of array after buy. Provides: np_t_open_v View of array after buy.
Provides: np_t_open_v_stop View of array after buy +1 Provides: np_t_open_v_stop View of array after buy +1
(Stop will search in here to prevent stopping in the past) (Stop will search in here to prevent stopping in the past)
""" """
np_t_open_v = np_bslap[t_open_ind:] np_t_open_v = np_bslap[t_open_ind:]
@ -446,13 +434,13 @@ class Backslapping:
''' '''
2 - Calculate our stop-loss price 2 - Calculate our stop-loss price
As stop is based on buy price of our trade As stop is based on buy price of our trade
- (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle - (BTO)Buys are Triggered On np_bto, typically the CLOSE of candle
- (BCO)Buys are Calculated On np_bco, default is OPEN of the next candle. - (BCO)Buys are Calculated On np_bco, default is OPEN of the next candle.
This is as we only see the CLOSE after it has happened. This is as we only see the CLOSE after it has happened.
The back test assumption is we have bought at first available price, the OPEN The back test assumption is we have bought at first available price, the OPEN
Requires: np_bslap - is our numpy array of the ticker DataFrame Requires: np_bslap - is our numpy array of the ticker DataFrame
Requires: t_open_ind - is the index row with the first buy. Requires: t_open_ind - is the index row with the first buy.
Requires: p_stop - is the stop rate, ie. 0.99 is -1% Requires: p_stop - is the stop rate, ie. 0.99 is -1%
@ -469,9 +457,9 @@ class Backslapping:
''' '''
3 - Find candle STO is under Stop-Loss After Trade opened. 3 - Find candle STO is under Stop-Loss After Trade opened.
where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri where [np_sto] (stop tiggered on variable: "close", "low" etc) < np_t_stop_pri
Requires: np_t_open_v_stop Numpy view of ticker_data after buy row +1 (when trade was opened) Requires: np_t_open_v_stop Numpy view of ticker_data after buy row +1 (when trade was opened)
Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close" Requires: np_sto User Var(STO)StopTriggeredOn. Typically set to "low" or "close"
Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop Requires: np_t_stop_pri The stop-loss price STO must fall under to trigger stop
@ -501,9 +489,9 @@ class Backslapping:
''' '''
4 - Find first sell index after trade open 4 - Find first sell index after trade open
First index in the view np_t_open_v where ['sell'] = 1 First index in the view np_t_open_v where ['sell'] = 1
Requires: np_t_open_v - view of ticker_data from buy onwards Requires: np_t_open_v - view of ticker_data from buy onwards
Requires: no_sell - integer '3', the buy column in the array Requires: no_sell - integer '3', the buy column in the array
Provides: np_t_sell_ind index of view where first sell=1 after buy Provides: np_t_sell_ind index of view where first sell=1 after buy
@ -528,13 +516,13 @@ class Backslapping:
''' '''
5 - Determine which was hit first a stop or sell 5 - Determine which was hit first a stop or sell
To then use as exit index price-field (sell on buy, stop on stop) To then use as exit index price-field (sell on buy, stop on stop)
STOP takes priority over SELL as would be 'in candle' from tick data STOP takes priority over SELL as would be 'in candle' from tick data
Sell would use Open from Next candle. Sell would use Open from Next candle.
So in a draw Stop would be hit first on ticker data in live So in a draw Stop would be hit first on ticker data in live
Validity of when types of trades may be executed can be summarised as: Validity of when types of trades may be executed can be summarised as:
Tick View Tick View
index index Buy Sell open low close high Stop price index index Buy Sell open low close high Stop price
open 2am 94 -1 0 0 ----- ------ ------ ----- ----- open 2am 94 -1 0 0 ----- ------ ------ ----- -----
@ -542,25 +530,25 @@ class Backslapping:
open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out open 4am 96 1 0 1 Enter trgstop trg sel ROI out Stop out
open 5am 97 2 0 0 Exit ------ ------- ----- ----- open 5am 97 2 0 0 Exit ------ ------- ----- -----
open 6am 98 3 0 0 ----- ------ ------- ----- ----- open 6am 98 3 0 0 ----- ------ ------- ----- -----
-1 means not found till end of view i.e no valid Stop found. Exclude from match. -1 means not found till end of view i.e no valid Stop found. Exclude from match.
Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid. Stop tiggering and closing in 96-1, the candle we bought at OPEN in, is valid.
Buys and sells are triggered at candle close Buys and sells are triggered at candle close
Both will open their postions at the open of the next candle. i/e + 1 index Both will open their postions at the open of the next candle. i/e + 1 index
Stop and buy Indexes are on the view. To map to the ticker dataframe Stop and buy Indexes are on the view. To map to the ticker dataframe
the t_open_ind index should be summed. the t_open_ind index should be summed.
np_t_stop_ind: Stop Found index in view np_t_stop_ind: Stop Found index in view
t_exit_ind : Sell found in view t_exit_ind : Sell found in view
t_open_ind : Where view was started on ticker_data t_open_ind : Where view was started on ticker_data
TODO: fix this frig for logic test,, case/switch/dictionary would be better... TODO: fix this frig for logic test,, case/switch/dictionary would be better...
more so when later testing many options, dynamic stop / roi etc more so when later testing many options, dynamic stop / roi etc
cludge - Setting np_t_sell_ind as 9999999999 when -1 (not found) cludge - Setting np_t_sell_ind as 9999999999 when -1 (not found)
cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found) cludge - Setting np_t_stop_ind as 9999999999 when -1 (not found)
''' '''
if debug: if debug:
print("\n(5) numpy debug\nStop or Sell Logic Processing") print("\n(5) numpy debug\nStop or Sell Logic Processing")
@ -730,7 +718,7 @@ class Backslapping:
if t_exit_last >= t_exit_ind or t_exit_last == -1: if t_exit_last >= t_exit_ind or t_exit_last == -1:
""" """
Break loop and go on to next pair. Break loop and go on to next pair.
When last trade exit equals index of last exit, there is no When last trade exit equals index of last exit, there is no
opportunity to close any more trades. opportunity to close any more trades.
""" """
@ -763,7 +751,7 @@ class Backslapping:
bslap_result["open_rate"] = round(np_trade_enter_price, 15) bslap_result["open_rate"] = round(np_trade_enter_price, 15)
bslap_result["close_rate"] = round(np_trade_exit_price, 15) bslap_result["close_rate"] = round(np_trade_exit_price, 15)
bslap_result["exit_type"] = t_exit_type bslap_result["exit_type"] = t_exit_type
bslap_result["sell_reason"] = t_exit_type #duplicated, but I don't care bslap_result["sell_reason"] = t_exit_type # duplicated, but I don't care
# append the dict to the list and print list # append the dict to the list and print list
bslap_pair_results.append(bslap_result) bslap_pair_results.append(bslap_result)
@ -787,3 +775,18 @@ class Backslapping:
# Send back List of trade dicts # Send back List of trade dicts
return bslap_pair_results return bslap_pair_results
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
backslapping = Backslapping(config)
backslapping.start()

View File

@ -4,51 +4,19 @@
This module contains the backtesting logic This module contains the backtesting logic
""" """
import logging import logging
import operator
from argparse import Namespace from argparse import Namespace
from datetime import datetime, timedelta from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow from pandas import DataFrame
from pandas import DataFrame, to_datetime
from tabulate import tabulate
import freqtrade.optimize as optimize from freqtrade.optimize.optimize import IOptimize, BacktestResult, OptimizeType, setup_configuration
from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json
from freqtrade.optimize.backslapping import Backslapping
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
from collections import OrderedDict
import timeit
from time import sleep
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple): class Backtesting(IOptimize):
"""
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
sell_reason: SellType
class Backtesting(object):
""" """
Backtesting class, this class contains all the logic to run a backtest Backtesting class, this class contains all the logic to run a backtest
@ -58,139 +26,8 @@ class Backtesting(object):
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config super().__init__(config)
self.strategy: IStrategy = StrategyResolver(self.config).strategy self._optimizetype = OptimizeType.BACKTEST
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
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()
self.stop_loss_value = self.strategy.stoploss
#### backslap config
'''
Numpy arrays are used for 100x speed up
We requires setting Int values for
buy stop triggers and stop calculated on
# buy 0 - open 1 - close 2 - sell 3 - high 4 - low 5 - stop 6
'''
self.np_buy: int = 0
self.np_open: int = 1
self.np_close: int = 2
self.np_sell: int = 3
self.np_high: int = 4
self.np_low: int = 5
self.np_stop: int = 6
self.np_bto: int = self.np_close # buys_triggered_on - should be close
self.np_bco: int = self.np_open # buys calculated on - open of the next candle.
self.np_sto: int = self.np_low # stops_triggered_on - Should be low, FT uses close
self.np_sco: int = self.np_stop # stops_calculated_on - Should be stop, FT uses close
# self.np_sto: int = self.np_close # stops_triggered_on - Should be low, FT uses close
# self.np_sco: int = self.np_close # stops_calculated_on - Should be stop, FT uses close
if 'backslap' in config:
self.use_backslap = config['backslap'] # Enable backslap - if false Orginal code is executed.
else:
self.use_backslap = False
logger.info("using backslap: {}".format(self.use_backslap))
self.debug = False # Main debug enable, very print heavy, enable 2 loops recommended
self.debug_timing = False # Stages within Backslap
self.debug_2loops = False # Limit each pair to two loops, useful when debugging
self.debug_vector = False # Debug vector calcs
self.debug_timing_main_loop = False # print overall timing per pair - works in Backtest and Backslap
self.backslap_show_trades = False # prints trades in addition to summary report
self.backslap_save_trades = True # saves trades as a pretty table to backslap.txt
self.stop_stops: int = 9999 # stop back testing any pair with this many stops, set to 999999 to not hit
self.backslap = Backslapping(config)
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
timeframe = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(),
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
tabular_data.append([
'TOTAL',
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 _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
@ -217,13 +54,14 @@ class Backtesting(object):
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal, sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
sell_row.sell) sell_row.sell)
if sell.sell_flag: if sell.sell_flag:
return BacktestResult(pair=pair, return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.open), profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=False, open_at_end=False,
@ -240,7 +78,7 @@ class Backtesting(object):
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True, open_at_end=True,
@ -253,14 +91,7 @@ class Backtesting(object):
return btr return btr
return None return None
def s(self): def run(self, args: Dict) -> DataFrame:
st = timeit.default_timer()
return st
def f(self, st):
return (timeit.default_timer() - st)
def backtest(self, args: Dict) -> DataFrame:
""" """
Implements backtesting functionality Implements backtesting functionality
@ -275,50 +106,32 @@ class Backtesting(object):
position_stacking: do we allow position stacking? (default: False) position_stacking: do we allow position stacking? (default: False)
:return: DataFrame :return: DataFrame
""" """
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
position_stacking = args.get('position_stacking', False)
trades = []
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
use_backslap = self.use_backslap ticker_data = self.advise_sell(
debug_timing = self.debug_timing_main_loop
if use_backslap: # Use Back Slap code
return self.backslap.run(args)
else: # use Original Back test code
########################## Original BT loop
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
position_stacking = args.get('position_stacking', False)
trades = []
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
if debug_timing: # Start timer
fl = self.s()
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.advise_sell(
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1) ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True) ticker_data.drop(ticker_data.head(1).index, inplace=True)
if debug_timing: # print time taken # Convert from Pandas to list for performance reasons
flt = self.f(fl) # (Looping Pandas is slow.)
# print("populate_buy_trend:", pair, round(flt, 10)) ticker = [x for x in ticker_data.itertuples()]
st = self.s()
# Convert from Pandas to list for performance reasons lock_pair_until = None
# (Looping Pandas is slow.) for index, row in enumerate(ticker):
ticker = [x for x in ticker_data.itertuples()] if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
lock_pair_until = None
for index, row in enumerate(ticker):
if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off
if not position_stacking: if not position_stacking:
if lock_pair_until is not None and row.date <= lock_pair_until: if lock_pair_until is not None and row.date <= lock_pair_until:
@ -328,178 +141,20 @@ class Backtesting(object):
if not trade_count_lock.get(row.date, 0) < max_open_trades: if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue continue
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:], trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args) trade_count_lock, args)
if trade_entry: if trade_entry:
lock_pair_until = trade_entry.close_time lock_pair_until = trade_entry.close_time
trades.append(trade_entry) trades.append(trade_entry)
else: else:
# Set lock_pair_until to end of testing period if trade could not be closed # 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 # This happens only if the buy-signal was with the last candle
lock_pair_until = ticker_data.iloc[-1].date lock_pair_until = ticker_data.iloc[-1].date
if debug_timing: # print time taken return DataFrame.from_records(trades, columns=BacktestResult._fields)
tt = self.f(st)
print("Time to BackTest :", pair, round(tt, 10))
print("-----------------------")
return DataFrame.from_records(trades, columns=BacktestResult._fields)
####################### Original BT loop end
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
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
)
ld_files = self.s()
if not data:
logger.critical("No data found. Terminating.")
return
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades']
else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0
preprocessed = self.tickerdata_to_dataframe(data)
t_t = self.f(ld_files)
print("Load from json to file to df in mem took", t_t)
# Print timeframe
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 = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'position_stacking': self.config.get('position_stacking', False),
}
)
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
if self.use_backslap:
logger.info(
'\n====================================================== '
'BackSLAP REPORT'
' =======================================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
# optional print trades
if self.backslap_show_trades:
TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs',
'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1)
def to_fwf(df, fname):
content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql')
print(content)
DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt")
# optional save trades
if self.backslap_save_trades:
TradesFrame = results.filter(['open_time', 'pair', 'exit_type', 'profit_percent', 'profit_abs',
'buy_spend', 'sell_take', 'trade_duration', 'close_time'], axis=1)
def to_fwf(df, fname):
content = tabulate(df.values.tolist(), list(df.columns), floatfmt=".8f", tablefmt='psql')
open(fname, "w").write(content)
DataFrame.to_fwf = to_fwf(TradesFrame, "backslap.txt")
else:
logger.info(
'\n================================================= '
'BACKTEST REPORT'
' ==================================================\n'
'%s',
self._generate_text_table(
data,
results
)
)
if 'sell_reason' in results.columns:
logger.info(
'\n' +
' SELL READON STATS '.center(119, '=') +
'\n%s \n',
self._generate_text_table_sell_reason(data, results)
)
else:
logger.info("no sell reasons available!")
logger.info(
'\n' +
' LEFT OPEN TRADES REPORT '.center(119, '=') +
'\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'] = ''
config['backslap'] = args.backslap
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: def start(args: Namespace) -> None:

View File

@ -24,6 +24,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.optimize import load_data from freqtrade.optimize import load_data
from freqtrade.optimize.optimize import OptimizeType
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -42,6 +43,7 @@ class Hyperopt(Backtesting):
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config) super().__init__(config)
self._optimizetype = OptimizeType.HYPEROPT
# set TARGET_TRADES to suit your number concurrent trades so its realistic # set TARGET_TRADES to suit your number concurrent trades so its realistic
# to the number of days # to the number of days
self.target_trades = 600 self.target_trades = 600
@ -276,7 +278,7 @@ class Hyperopt(Backtesting):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE) processed = load(TICKERDATA_PICKLE)
results = self.backtest( results = self.run(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,

View File

@ -0,0 +1,329 @@
# pragma pylint: disable=missing-docstring, W0212, too-many-arguments
"""
This module contains the backtesting logic
"""
import logging
import operator
from abc import ABC, abstractmethod
from argparse import Namespace
from copy import deepcopy
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
from enum import Enum
import arrow
from pandas import DataFrame
from tabulate import tabulate
from freqtrade import DependencyException, constants
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json
import freqtrade.optimize as optimize
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
open_rate: float
close_rate: float
sell_reason: SellType
class OptimizeType(Enum):
BACKTEST = "backtest"
BACKSLAP = "backslap"
HYPEROPT = "hyperopt"
class IOptimize(ABC):
"""
Backtesting Abstract 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
# 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.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.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
def _get_timeframe(self, data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
timeframe = [
(arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values()
]
return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(),
str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
tabular_data.append([
'TOTAL',
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 _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
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(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records:
if 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)
file_dump_json(recordfilename, records)
def start(self) -> None:
"""
Run a backtesting end-to-end
:return: None
"""
data = {}
pairs = self.config['exchange']['pair_whitelist']
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
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
# Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades']
else:
logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
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)
# Print timeframe
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
all_results[self.strategy.get_strategy_name()] = self.run(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'position_stacking': self.config.get('position_stacking', False),
}
)
for strategy, results in all_results.items():
if self.config.get('export', False):
self._store_backtest_result(self.config['exportfilename'], results,
strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}")
print(f' {self._optimizetype.value.upper()} REPORT '.center(119, '='))
print(self._generate_text_table(data, results))
print(' SELL REASON STATS '.center(119, '='))
print(self._generate_text_table_sell_reason(data, results))
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
print(self._generate_text_table(data, results.loc[results.open_at_end]))
print()
if len(all_results) > 1:
# Print Strategy summary table
print(' Strategy Summary '.center(119, '='))
print(self._generate_text_table_strategy(all_results))
print('\nFor more details, please look at the detail tables above')
@abstractmethod
def run(self, args: Dict) -> DataFrame:
"""
Runs backtesting functionality.
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)
position_stacking: do we allow position stacking? (default: False)
:return: DataFrame
"""
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

View File

@ -82,7 +82,7 @@ def check_migrate(engine) -> None:
logger.info(f'trying {table_back_name}') logger.info(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'):
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 +157,8 @@ class Trade(_DECL_BASE):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float) open_rate = Column(Float)

View File

@ -524,7 +524,7 @@ def make_fetch_ohlcv_mock(data):
return fetch_ohlcv_mock return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker): def test_get_candle_history(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
tick = [ tick = [
[ [
@ -541,7 +541,7 @@ def test_get_ticker_history(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker # retrieve original ticker
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000 assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1 assert ticks[0][1] == 1
assert ticks[0][2] == 2 assert ticks[0][2] == 2
@ -563,7 +563,7 @@ def test_get_ticker_history(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000 assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6 assert ticks[0][1] == 6
assert ticks[0][2] == 7 assert ticks[0][2] == 7
@ -572,16 +572,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)
@ -604,7 +604,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000 assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649 assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651 assert ticks[0][2] == 0.07651
@ -637,7 +637,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000 assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999 assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766 assert ticks[0][2] == 0.0766

View File

@ -91,7 +91,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
data = load_data_test(contour) data = load_data_test(contour)
processed = backtesting.tickerdata_to_dataframe(data) processed = backtesting.tickerdata_to_dataframe(data)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtesting.backtest( results = backtesting.run(
{ {
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
'processed': processed, 'processed': processed,
@ -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)
@ -347,7 +347,7 @@ def test_get_timeframe(default_conf, mocker) -> None:
pairs=['UNITTEST/BTC'] pairs=['UNITTEST/BTC']
) )
) )
min_date, max_date = backtesting.get_timeframe(data) min_date, max_date = backtesting._get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
@ -406,18 +406,62 @@ 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',
backtest=MagicMock(), run=MagicMock(),
_generate_text_table=MagicMock(return_value='1'), _generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe, _get_timeframe=get_timeframe,
) )
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
@ -446,13 +490,13 @@ 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',
backtest=MagicMock(), run=MagicMock(),
_generate_text_table=MagicMock(return_value='1'), _generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe, _get_timeframe=get_timeframe,
) )
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
@ -477,7 +521,7 @@ def test_backtest(default_conf, fee, mocker) -> None:
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
data_processed = backtesting.tickerdata_to_dataframe(data) data_processed = backtesting.tickerdata_to_dataframe(data)
results = backtesting.backtest( results = backtesting.run(
{ {
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'processed': data_processed, 'processed': data_processed,
@ -524,7 +568,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
# Run a backtesting for an exiting 5min ticker_interval # Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtesting.backtest( results = backtesting.run(
{ {
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data), 'processed': backtesting.tickerdata_to_dataframe(data),
@ -568,7 +612,7 @@ def test_backtest_ticks(default_conf, fee, mocker):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.advise_buy = fun # Override backtesting.advise_buy = fun # Override
backtesting.advise_sell = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.run(backtest_conf)
assert not results.empty assert not results.empty
@ -583,7 +627,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.advise_buy = fun # Override backtesting.advise_buy = fun # Override
backtesting.advise_sell = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.run(backtest_conf)
assert results.empty assert results.empty
@ -598,7 +642,7 @@ def test_backtest_only_sell(mocker, default_conf):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.advise_buy = fun # Override backtesting.advise_buy = fun # Override
backtesting.advise_sell = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.run(backtest_conf)
assert results.empty assert results.empty
@ -608,7 +652,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.advise_buy = _trend_alternate # Override backtesting.advise_buy = _trend_alternate # Override
backtesting.advise_sell = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override
results = backtesting.backtest(backtest_conf) results = backtesting.run(backtest_conf)
backtesting._store_backtest_result("test_.json", results) backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4 assert len(results) == 4
# One trade was force-closed at the end # One trade was force-closed at the end
@ -621,7 +665,7 @@ def test_backtest_record(default_conf, fee, mocker):
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch( mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json', 'freqtrade.optimize.optimize.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r)) new=lambda n, r: (names.append(n), records.append(r))
) )
@ -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,24 +733,15 @@ 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.run', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(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.run', 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

@ -263,7 +263,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
backtest_result = pd.DataFrame.from_records(trades, columns=labels) backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest', 'freqtrade.optimize.hyperopt.Hyperopt.run',
MagicMock(return_value=backtest_result) MagicMock(return_value=backtest_result)
) )
patch_exchange(mocker) patch_exchange(mocker)

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

@ -88,7 +88,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',

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:
@ -544,7 +544,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,
) )

View File

@ -404,6 +404,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
amount = 103.223 amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL, id INTEGER NOT NULL,
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
@ -418,14 +419,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

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,4 +1,4 @@
ccxt==1.17.60 ccxt==1.17.118
SQLAlchemy==1.2.10 SQLAlchemy==1.2.10
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
@ -6,13 +6,13 @@ 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.0
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.7.0 pytest==3.7.1
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

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"),