diff --git a/docs/backtesting.md b/docs/backtesting.md index 8574d9dc2..9829a10c8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -51,6 +51,29 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 ``` +**Running backtest with smaller testset** +Use the --timerange argument to change how much of the testset +you want to use. The last N ticks/timeframes will be used. +Example: + +```bash +python3 ./freqtrade/main.py backtesting --timerange=-200 +``` + +***Advanced use of timerange*** + Doing --timerange=-200 will get the last 200 timeframes + from your inputdata. You can also specify specific dates, + or a range span indexed by start and stop. + The full timerange specification: + Not implemented yet! --timerange=-20180131 + Not implemented yet! --timerange=20180101- + Not implemented yet! --timerange=20180101-20181231 + Last 123 tickframes of data: --timerange=-123 + First 123 tickframes of data: --timerange=123- + Tickframes from line 123 through 456: --timerange=123-456 + + +**Update testdata directory To update your testdata directory, or download into another testdata directory: ```bash mkdir freqtrade/tests/testdata-20180113 diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 24a9dbc51..af564f0b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -168,6 +168,16 @@ If you would like to learn parameters using an alternate ticke-data that you have on-disk, use the --datadir PATH option. Default hyperopt will use data from directory freqtrade/tests/testdata. +### Running hyperopt with smaller testset + +Use the --timeperiod argument to change how much of the testset +you want to use. The last N ticks/timeframes will be used. +Example: + +```bash +python3 ./freqtrade/main.py hyperopt --timeperiod -200 +``` + ### Hyperopt with MongoDB Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by executing the previous command is the execution takes a long time. diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 979174f8d..a9aeee80e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -4,6 +4,7 @@ import json import logging import time import os +import re from typing import Any, Callable, Dict, List from jsonschema import Draft4Validator, validate @@ -132,7 +133,7 @@ def parse_args(args: List[str], description: str): dest='dry_run_db', ) parser.add_argument( - '-dd', '--datadir', + '--datadir', help='path to backtest data (default freqdata/tests/testdata', dest='datadir', default=os.path.join('freqtrade', 'tests', 'testdata'), @@ -190,6 +191,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: action='store_true', dest='refresh_pairs', ) + backtesting_cmd.add_argument( + '--timerange', + help='Specify what timerange of data to use.', + default=None, + type=str, + dest='timerange', + ) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') @@ -216,6 +224,43 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) + hyperopt_cmd.add_argument( + '--timerange', + help='Specify what timerange of data to use.', + default=None, + type=str, + dest='timerange', + ) + + +def parse_timerange(text): + if text is None: + return None + syntax = [('^-(\d{8})$', (None, 'date')), + ('^(\d{8})-$', ('date', None)), + ('^(\d{8})-(\d{8})$', ('date', 'date')), + ('^(-\d+)$', (None, 'line')), + ('^(\d+)-$', ('line', None)), + ('^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + m = re.match(rex, text) + if m: # Regex has matched + rvals = m.groups() + n = 0 + start = None + stop = None + if stype[0]: + start = rvals[n] + if stype[0] != 'date': + start = int(start) + n += 1 + if stype[1]: + stop = rvals[n] + if stype[1] != 'date': + stop = int(stop) + return (stype, start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) # Required json-schema for user specified config diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 2d73c3215..40269db46 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -12,7 +12,20 @@ from freqtrade.analyze import populate_indicators, parse_ticker_dataframe logger = logging.getLogger(__name__) -def load_tickerdata_file(datadir, pair, ticker_interval): +def trim_tickerlist(tickerlist, timerange): + (stype, start, stop) = timerange + if stype == (None, 'line'): + return tickerlist[stop:] + elif stype == ('line', None): + return tickerlist[0:start] + elif stype == ('index', 'index'): + return tickerlist[start:stop] + else: + return tickerlist + + +def load_tickerdata_file(datadir, pair, ticker_interval, + timerange=None): """ Load a pair from file, :return dict OR empty if unsuccesful @@ -30,11 +43,15 @@ def load_tickerdata_file(datadir, pair, ticker_interval): # Read the file, load the json with open(file) as tickerdata: pairdata = json.load(tickerdata) + if timerange: + pairdata = trim_tickerlist(pairdata, timerange) return pairdata -def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None, - refresh_pairs: Optional[bool] = False) -> Dict[str, List]: +def load_data(datadir: str, ticker_interval: int = 5, + pairs: Optional[List[str]] = None, + refresh_pairs: Optional[bool] = False, + timerange=None) -> Dict[str, List]: """ Loads ticker history data for the given parameters :param ticker_interval: ticker interval in minutes @@ -51,16 +68,21 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] download_pairs(datadir, _pairs) for pair in _pairs: - pairdata = load_tickerdata_file(datadir, pair, ticker_interval) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) if not pairdata: # download the tickerdata from exchange download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval) # and retry reading the pair - pairdata = load_tickerdata_file(datadir, pair, ticker_interval) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) result[pair] = pairdata return result +def tickerdata_to_dataframe(data): + preprocessed = preprocess(data) + return preprocessed + + def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """Creates a dataframe and populates indicators for given ticker data""" return {pair: populate_indicators(parse_ticker_dataframe(pair_data)) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6d600b303..4f3d4bb24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,6 @@ from freqtrade import exchange from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached -from freqtrade.optimize import preprocess from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -161,12 +160,13 @@ def start(args): data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) else: logger.info('Using local backtesting data (using whitelist in given config) ...') - data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, - refresh_pairs=args.refresh_pairs) - logger.info('Using stake_currency: %s ...', config['stake_currency']) logger.info('Using stake_amount: %s ...', config['stake_amount']) + timerange = misc.parse_timerange(args.timerange) + data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, + refresh_pairs=args.refresh_pairs, + timerange=timerange) max_open_trades = 0 if args.realistic_simulation: logger.info('Using max_open_trades: %s ...', config['max_open_trades']) @@ -176,7 +176,7 @@ def start(args): from freqtrade import main main._CONF = config - preprocessed = preprocess(data) + preprocessed = optimize.tickerdata_to_dataframe(data) # Print timeframe min_date, max_date = get_timeframe(preprocessed) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 959e64206..b98646090 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -15,7 +15,7 @@ from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from hyperopt.mongoexp import MongoTrials from pandas import DataFrame -from freqtrade import main # noqa +from freqtrade import main, misc # noqa from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.misc import load_config @@ -273,8 +273,11 @@ def start(args): logger.info('Using config: %s ...', args.config) config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] - PROCESSED = optimize.preprocess(optimize.load_data( - args.datadir, pairs=pairs, ticker_interval=args.ticker_interval)) + timerange = misc.parse_timerange(args.timerange) + data = optimize.load_data(args.datadir, pairs=pairs, + ticker_interval=args.ticker_interval, + timerange=timerange) + PROCESSED = optimize.tickerdata_to_dataframe(data) if args.mongodb: logger.info('Using mongodb ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 4ef4b1606..c570801c5 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -69,8 +69,8 @@ def test_backtest_1min_ticker_interval(default_conf, mocker): def load_data_test(what): - data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST']) - data = trim_dictlist(data, -100) + timerange = ((None, 'line'), None, -100) + data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange) pair = data['BTC_UNITEST'] datalen = len(pair) # Depending on the what parameter we now adjust the @@ -152,10 +152,10 @@ def test_backtest_pricecontours(default_conf, mocker): simple_backtest(default_conf, contour, numres) -def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False): - tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1) +def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None): + tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange) pairdata = {'BTC_UNITEST': tickerdata} - return trim_dictlist(pairdata, -100) + return pairdata def test_backtest_start(default_conf, mocker, caplog): @@ -169,6 +169,7 @@ def test_backtest_start(default_conf, mocker, caplog): args.level = 10 args.live = False args.datadir = None + args.timerange = '-100' # needed due to MagicMock malleability backtesting.start(args) # check the logs, that will contain the backtest result exists = ['Using max_open_trades: 1 ...', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 4bb5c8c0b..6e21cde76 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -54,6 +54,7 @@ def create_trials(mocker): def test_start_calls_fmin(mocker): trials = create_trials(mocker) + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials) mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results) @@ -61,7 +62,8 @@ def test_start_calls_fmin(mocker): mocker.patch('freqtrade.optimize.load_data') mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) - args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False) + args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False, + timerange=None) start(args) mock_fmin.assert_called_once() @@ -70,11 +72,12 @@ def test_start_calls_fmin(mocker): def test_start_uses_mongotrials(mocker): mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) - args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True) + args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True, + timerange=None) start(args) mock_mongotrials.assert_called_once() @@ -125,11 +128,12 @@ def test_fmin_best_results(mocker, caplog): } mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) - args = mocker.Mock(epochs=1, config='config.json.example') + args = mocker.Mock(epochs=1, config='config.json.example', + timerange=None) start(args) exists = [ @@ -147,11 +151,12 @@ def test_fmin_best_results(mocker, caplog): def test_fmin_throw_value_error(mocker, caplog): mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError()) - args = mocker.Mock(epochs=1, config='config.json.example') + args = mocker.Mock(epochs=1, config='config.json.example', + timerange=None) start(args) exists = [ @@ -185,7 +190,8 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker): return_value={}) args = mocker.Mock(epochs=1, config='config.json.example', - mongodb=False) + mongodb=False, + timerange=None) start(args) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 57c41c9c6..61325797b 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -174,3 +174,11 @@ def test_load_tickerdata_file(): assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1) assert _btc_unittest_length == len(tickerdata) + + +def test_tickerdata_to_dataframe(): + timerange = ((None, 'line'), None, -100) + tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange) + tickerlist = {'BTC_UNITEST': tick} + data = optimize.tickerdata_to_dataframe(tickerlist) + assert 100 == len(data['BTC_UNITEST']) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 63cfba627..74f611f5f 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -8,7 +8,7 @@ import pytest from jsonschema import ValidationError from freqtrade.misc import (common_args_parser, load_config, parse_args, - throttle) + throttle, parse_timerange) def test_throttle(): @@ -133,6 +133,13 @@ def test_parse_args_hyperopt_custom(mocker): assert call_args.func is not None +def test_parse_timerange_incorrect(): + assert ((None, 'line'), None, -200) == parse_timerange('-200') + assert (('line', None), 200, None) == parse_timerange('200-') + with pytest.raises(Exception, match=r'Incorrect syntax.*'): + parse_timerange('-') + + def test_load_config(default_conf, mocker): file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( read_data=json.dumps(default_conf)