Merge pull request #361 from kryofly/backtest-export

Backtest export
This commit is contained in:
Janne Sinivirta 2018-01-20 07:45:38 +02:00 committed by GitHub
commit 99de17da82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 125 additions and 63 deletions

View File

@ -51,6 +51,11 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
``` ```
**Exporting trades to file**
```bash
freqtrade backtesting --export trades
```
**Running backtest with smaller testset** **Running backtest with smaller testset**
Use the `--timerange` argument to change how much of the testset Use the `--timerange` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used. you want to use. The last N ticks/timeframes will be used.

View File

@ -191,6 +191,14 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
action='store_true', action='store_true',
dest='refresh_pairs', dest='refresh_pairs',
) )
backtesting_cmd.add_argument(
'--export',
help='Export backtest results, argument are: trades\
Example --export trades',
type=str,
default=None,
dest='export',
)
backtesting_cmd.add_argument( backtesting_cmd.add_argument(
'--timerange', '--timerange',
help='Specify what timerange of data to use.', help='Specify what timerange of data to use.',

View File

@ -8,6 +8,7 @@ from pandas import DataFrame
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
from freqtrade import misc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -149,7 +150,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
logger.debug("New End: {}".format(data[-1:][0]['T'])) logger.debug("New End: {}".format(data[-1:][0]['T']))
data = sorted(data, key=lambda data: data['T']) data = sorted(data, key=lambda data: data['T'])
with open(filename, "wt") as fp: misc.file_dump_json(filename, data)
json.dump(data, fp)
return True return True

View File

@ -66,17 +66,60 @@ def generate_text_table(
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt) return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
def backtest(stake_amount: float, processed: Dict[str, DataFrame], def get_trade_entry(pair, row, ticker, trade_count_lock, args):
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False, stake_amount = args['stake_amount']
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame: max_open_trades = args.get('max_open_trades', 0)
sell_profit_only = args.get('sell_profit_only', False)
stoploss = args.get('stoploss', -1)
use_sell_signal = args.get('use_sell_signal', False)
trade = Trade(open_rate=row.close,
open_date=row.date,
stake_amount=stake_amount,
amount=stake_amount / row.open,
fee=exchange.get_fee()
)
# calculate win/lose forwards from buy point
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
for row2 in sell_subset.itertuples(index=True):
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
if (sell_profit_only and current_profit_percent < 0):
continue
if min_roi_reached(trade, row2.close, row2.date) or \
(row2.sell == 1 and use_sell_signal) or \
current_profit_percent <= stoploss:
current_profit_btc = trade.calc_profit(rate=row2.close)
return row2, (pair,
current_profit_percent,
current_profit_btc,
row2.Index - row.Index,
current_profit_btc > 0,
current_profit_btc < 0
)
def backtest(args) -> DataFrame:
""" """
Implements backtesting functionality Implements backtesting functionality
:param stake_amount: btc amount to use for each trade :param args: a dict containing:
:param processed: a processed dictionary with format {pair, data} stake_amount: btc amount to use for each trade
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled) processed: a processed dictionary with format {pair, data}
:param realistic: do we try to simulate realistic trades? (default: True) max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
stoploss: use stoploss
:return: DataFrame :return: DataFrame
""" """
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', True)
record = args.get('record', None)
records = []
trades = [] trades = []
trade_count_lock: dict = {} trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
@ -99,41 +142,25 @@ def backtest(stake_amount: float, processed: Dict[str, DataFrame],
# Increase lock # Increase lock
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 = Trade( ret = get_trade_entry(pair, row, ticker,
open_rate=row.close, trade_count_lock, args)
open_date=row.date, if ret:
stake_amount=stake_amount, row2, trade_entry = ret
amount=stake_amount / row.open, lock_pair_until = row2.Index
fee=exchange.get_fee() trades.append(trade_entry)
) if record:
# Note, need to be json.dump friendly
# calculate win/lose forwards from buy point # record a tuple of pair, current_profit_percent,
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']] # entry-date, duration
for row2 in sell_subset.itertuples(index=True): records.append((pair, trade_entry[1],
if max_open_trades > 0: row.date.strftime('%s'),
# Increase trade_count_lock for every iteration row2.date.strftime('%s'),
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 row.Index, trade_entry[3]))
# For now export inside backtest(), maybe change so that backtest()
current_profit_percent = trade.calc_profit_percent(rate=row2.close) # returns a tuple like: (dataframe, records, logs, etc)
if (sell_profit_only and current_profit_percent < 0): if record and record.find('trades') >= 0:
continue logger.info('Dumping backtest results')
if min_roi_reached(trade, row2.close, row2.date) or \ misc.file_dump_json('backtest-result.json', records)
(row2.sell == 1 and use_sell_signal) or \
current_profit_percent <= stoploss:
current_profit_btc = trade.calc_profit(rate=row2.close)
lock_pair_until = row2.Index
trades.append(
(
pair,
current_profit_percent,
current_profit_btc,
row2.Index - row.Index,
current_profit_btc > 0,
current_profit_btc < 0
)
)
break
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss'] labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
@ -180,17 +207,18 @@ def start(args):
# Print timeframe # Print timeframe
min_date, max_date = get_timeframe(preprocessed) min_date, max_date = get_timeframe(preprocessed)
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
# Execute backtest and print results # Execute backtest and print results
results = backtest( sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
stake_amount=config['stake_amount'], use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
processed=preprocessed, results = backtest({'stake_amount': config['stake_amount'],
max_open_trades=max_open_trades, 'processed': preprocessed,
realistic=args.realistic_simulation, 'max_open_trades': max_open_trades,
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False), 'realistic': args.realistic_simulation,
stoploss=config.get('stoploss'), 'sell_profit_only': sell_profit_only,
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False) 'use_sell_signal': use_sell_signal,
) 'stoploss': config.get('stoploss'),
'record': args.export
})
logger.info( logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa '\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
generate_text_table(data, results, config['stake_currency'], args.ticker_interval) generate_text_table(data, results, config['stake_currency'], args.ticker_interval)

View File

@ -164,7 +164,9 @@ def optimizer(params):
from freqtrade.optimize import backtesting from freqtrade.optimize import backtesting
backtesting.populate_buy_trend = buy_strategy_generator(params) backtesting.populate_buy_trend = buy_strategy_generator(params)
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss']) results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'],
'processed': PROCESSED,
'stoploss': params['stoploss']})
result_explanation = format_results(results) result_explanation = format_results(results)
total_profit = results.profit_percent.sum() total_profit = results.profit_percent.sum()

View File

@ -51,8 +51,10 @@ def test_backtest(default_conf, mocker):
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest(default_conf['stake_amount'], results = backtest({'stake_amount': default_conf['stake_amount'],
optimize.preprocess(data), 10, True) 'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True})
assert not results.empty assert not results.empty
@ -63,8 +65,10 @@ def test_backtest_1min_ticker_interval(default_conf, mocker):
# 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=1, pairs=['BTC_UNITEST']) data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest(default_conf['stake_amount'], results = backtest({'stake_amount': default_conf['stake_amount'],
optimize.preprocess(data), 1, True) 'processed': optimize.preprocess(data),
'max_open_trades': 1,
'realistic': True})
assert not results.empty assert not results.empty
@ -115,7 +119,10 @@ def simple_backtest(config, contour, num_results):
data = load_data_test(contour) data = load_data_test(contour)
processed = optimize.preprocess(data) processed = optimize.preprocess(data)
assert isinstance(processed, dict) assert isinstance(processed, dict)
results = backtest(config['stake_amount'], processed, 1, True) results = backtest({'stake_amount': config['stake_amount'],
'processed': processed,
'max_open_trades': 1,
'realistic': True})
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results assert len(results) == num_results
@ -128,8 +135,10 @@ def test_backtest2(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest(default_conf['stake_amount'], results = backtest({'stake_amount': default_conf['stake_amount'],
optimize.preprocess(data), 10, True) 'processed': optimize.preprocess(data),
'max_open_trades': 10,
'realistic': True})
assert not results.empty assert not results.empty
@ -169,6 +178,7 @@ def test_backtest_start(default_conf, mocker, caplog):
args.level = 10 args.level = 10
args.live = False args.live = False
args.datadir = None args.datadir = None
args.export = None
args.timerange = '-100' # needed due to MagicMock malleability args.timerange = '-100' # needed due to MagicMock malleability
backtesting.start(args) backtesting.start(args)
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result

View File

@ -5,10 +5,11 @@ import time
from copy import deepcopy from copy import deepcopy
import pytest import pytest
from unittest.mock import MagicMock
from jsonschema import ValidationError from jsonschema import ValidationError
from freqtrade.misc import (common_args_parser, load_config, parse_args, from freqtrade.misc import (common_args_parser, load_config, parse_args,
throttle, parse_timerange) throttle, file_dump_json, parse_timerange)
def test_throttle(): def test_throttle():
@ -133,6 +134,14 @@ def test_parse_args_hyperopt_custom(mocker):
assert call_args.func is not None assert call_args.func is not None
def test_file_dump_json(default_conf, mocker):
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
def test_parse_timerange_incorrect(): def test_parse_timerange_incorrect():
assert ((None, 'line'), None, -200) == parse_timerange('-200') assert ((None, 'line'), None, -200) == parse_timerange('-200')
assert (('line', None), 200, None) == parse_timerange('200-') assert (('line', None), 200, None) == parse_timerange('200-')