diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 784e4903e..5242240e0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -128,18 +128,20 @@ def parse_args(args: List[str]): def build_subcommands(parser: argparse.ArgumentParser) -> None: """ Builds and attaches all subcommands """ - from freqtrade.optimize import backtesting + from freqtrade.optimize import backtesting, hyperopt subparsers = parser.add_subparsers(dest='subparser') - backtest = subparsers.add_parser('backtesting', help='backtesting module') - backtest.set_defaults(func=backtesting.start) - backtest.add_argument( + + # Add backtesting subcommand + backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module') + backtesting_cmd.set_defaults(func=backtesting.start) + backtesting_cmd.add_argument( '-l', '--live', action='store_true', dest='live', help='using live data', ) - backtest.add_argument( + backtesting_cmd.add_argument( '-i', '--ticker-interval', help='specify ticker interval in minutes (default: 5)', dest='ticker_interval', @@ -147,13 +149,17 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) - backtest.add_argument( + backtesting_cmd.add_argument( '--realistic-simulation', help='uses max_open_trades from config to simulate real world limitations', action='store_true', dest='realistic_simulation', ) + # Add hyperopt subcommand + hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') + hyperopt_cmd.set_defaults(func=hyperopt.start) + # Required json-schema for user specified config CONF_SCHEMA = { diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 4cffcee10..1bd1a97e8 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1 +1,41 @@ -from . import backtesting +# pragma pylint: disable=missing-docstring + + +import json +import os +from typing import Optional, List, Dict + +from pandas import DataFrame + +from freqtrade.analyze import populate_indicators, parse_ticker_dataframe + + +def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None) -> Dict[str, List]: + """ + Loads ticker history data for the given parameters + :param ticker_interval: ticker interval in minutes + :param pairs: list of pairs + :return: dict + """ + path = os.path.abspath(os.path.dirname(__file__)) + result = {} + _pairs = pairs or [ + 'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC', + 'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK', + ] + for pair in _pairs: + with open('{abspath}/../tests/testdata/{pair}-{ticker_interval}.json'.format( + abspath=path, + pair=pair, + ticker_interval=ticker_interval, + )) as tickerdata: + result[pair] = json.load(tickerdata) + return result + + +def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: + """Creates a dataframe and populates indicators for given ticker data""" + processed = {} + for pair, pair_data in tickerdata.items(): + processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data)) + return processed diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ea861ee84..508d4755d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,34 +9,17 @@ from pandas import DataFrame from tabulate import tabulate from freqtrade import exchange -from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \ - populate_buy_trend, populate_sell_trend +from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached from freqtrade.misc import load_config +from freqtrade.optimize import load_data, preprocess from freqtrade.persistence import Trade -from freqtrade.tests import load_backtesting_data + logger = logging.getLogger(__name__) -def format_results(results: DataFrame): - return ('Made {:6d} buys. Average profit {: 5.2f}%. ' - 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( - len(results.index), - results.profit.mean() * 100.0, - results.profit.sum(), - results.duration.mean() * 5, - ) - - -def preprocess(backdata) -> Dict[str, DataFrame]: - processed = {} - for pair, pair_data in backdata.items(): - processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data)) - return processed - - def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ Get the maximum timeframe for the given backtest data @@ -151,7 +134,7 @@ def start(args): data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) else: print('Using local backtesting data (ignoring whitelist in given config)...') - data = load_backtesting_data(args.ticker_interval) + data = load_data(args.ticker_interval) print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format( config['stake_currency'], config['stake_amount'] diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5face5837..b3c35710b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -1,29 +1,124 @@ # pragma pylint: disable=missing-docstring,W0212 -import logging -import os + + from functools import reduce from math import exp from operator import itemgetter -import pytest from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from pandas import DataFrame -from freqtrade import exchange +from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex -from freqtrade.optimize.backtesting import backtest, format_results -from freqtrade.optimize.backtesting import preprocess -from freqtrade.tests import load_backtesting_data +from freqtrade.optimize.backtesting import backtest from freqtrade.vendor.qtpylib.indicators import crossed_above -logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot - # set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data TARGET_TRADES = 1100 TOTAL_TRIES = 4 # pylint: disable=C0103 current_tries = 0 +# Configuration and data used by hyperopt +PROCESSED = optimize.preprocess(optimize.load_data()) +OPTIMIZE_CONFIG = { + 'max_open_trades': 3, + 'stake_currency': 'BTC', + 'stake_amount': 0.01, + 'minimal_roi': { + '40': 0.0, + '30': 0.01, + '20': 0.02, + '0': 0.04, + }, + 'stoploss': -0.10, +} + +SPACE = { + 'mfi': hp.choice('mfi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} + ]), + 'fastd': hp.choice('fastd', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} + ]), + 'adx': hp.choice('adx', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} + ]), + 'rsi': hp.choice('rsi', [ + {'enabled': False}, + {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} + ]), + 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ + {'enabled': False}, + {'enabled': True} + ]), + 'over_sar': hp.choice('over_sar', [ + {'enabled': False}, + {'enabled': True} + ]), + 'green_candle': hp.choice('green_candle', [ + {'enabled': False}, + {'enabled': True} + ]), + 'uptrend_sma': hp.choice('uptrend_sma', [ + {'enabled': False}, + {'enabled': True} + ]), + 'trigger': hp.choice('trigger', [ + {'type': 'lower_bb'}, + {'type': 'faststoch10'}, + {'type': 'ao_cross_zero'}, + {'type': 'ema5_cross_ema10'}, + {'type': 'macd_cross_signal'}, + {'type': 'sar_reversal'}, + {'type': 'stochf_cross'}, + {'type': 'ht_sine'}, + ]), +} + + +def optimizer(params): + from freqtrade.optimize import backtesting + backtesting.populate_buy_trend = buy_strategy_generator(params) + + results = backtest(OPTIMIZE_CONFIG, PROCESSED) + + result = format_results(results) + + total_profit = results.profit.sum() * 1000 + trade_count = len(results.index) + + trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) + profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000 + + # pylint: disable=W0603 + global current_tries + current_tries += 1 + print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result)) + + return { + 'loss': trade_loss + profit_loss, + 'status': STATUS_OK, + 'result': result + } + + +def format_results(results: DataFrame): + return ('Made {:6d} buys. Average profit {: 5.2f}%. ' + 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( + len(results.index), + results.profit.mean() * 100.0, + results.profit.sum(), + results.duration.mean() * 5, + ) + def buy_strategy_generator(params): def populate_buy_trend(dataframe: DataFrame) -> DataFrame: @@ -70,94 +165,14 @@ def buy_strategy_generator(params): return populate_buy_trend -@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") -def test_hyperopt(backtest_conf, mocker): - mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend') +def start(args): + # TODO: parse args - backdata = load_backtesting_data() - processed = preprocess(backdata) exchange._API = Bittrex({'key': '', 'secret': ''}) - def optimizer(params): - mocked_buy_trend.side_effect = buy_strategy_generator(params) - - results = backtest(backtest_conf, processed, mocker) - - result = format_results(results) - - total_profit = results.profit.sum() * 1000 - trade_count = len(results.index) - - trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) - profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000 - - # pylint: disable=W0603 - global current_tries - current_tries += 1 - print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result)) - - return { - 'loss': trade_loss + profit_loss, - 'status': STATUS_OK, - 'result': result - } - - space = { - 'mfi': hp.choice('mfi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)} - ]), - 'fastd': hp.choice('fastd', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)} - ]), - 'adx': hp.choice('adx', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)} - ]), - 'rsi': hp.choice('rsi', [ - {'enabled': False}, - {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)} - ]), - 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_short_ema': hp.choice('uptrend_short_ema', [ - {'enabled': False}, - {'enabled': True} - ]), - 'over_sar': hp.choice('over_sar', [ - {'enabled': False}, - {'enabled': True} - ]), - 'green_candle': hp.choice('green_candle', [ - {'enabled': False}, - {'enabled': True} - ]), - 'uptrend_sma': hp.choice('uptrend_sma', [ - {'enabled': False}, - {'enabled': True} - ]), - 'trigger': hp.choice('trigger', [ - {'type': 'lower_bb'}, - {'type': 'faststoch10'}, - {'type': 'ao_cross_zero'}, - {'type': 'ema5_cross_ema10'}, - {'type': 'macd_cross_signal'}, - {'type': 'sar_reversal'}, - {'type': 'stochf_cross'}, - {'type': 'ht_sine'}, - ]), - } trials = Trials() - best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) + best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials) print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================') print('Best parameters {}'.format(best)) newlist = sorted(trials.results, key=itemgetter('loss')) print('Result: {}'.format(newlist[0]['result'])) - - -if __name__ == '__main__': - # for profiling with cProfile and line_profiler - pytest.main([__file__, '-s']) diff --git a/freqtrade/tests/__init__.py b/freqtrade/tests/__init__.py index ebebe7c98..e69de29bb 100644 --- a/freqtrade/tests/__init__.py +++ b/freqtrade/tests/__init__.py @@ -1,21 +0,0 @@ -# pragma pylint: disable=missing-docstring -import json -import os -from typing import Optional, List - - -def load_backtesting_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None): - path = os.path.abspath(os.path.dirname(__file__)) - result = {} - _pairs = pairs or [ - 'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC', - 'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK', - ] - for pair in _pairs: - with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format( - abspath=path, - pair=pair, - ticker_interval=ticker_interval, - )) as tickerdata: - result[pair] = json.load(tickerdata) - return result diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index e624e96c7..f2a9362ec 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -51,22 +51,6 @@ def default_conf(): return configuration -@pytest.fixture(scope="module") -def backtest_conf(): - return { - "max_open_trades": 3, - "stake_currency": "BTC", - "stake_amount": 0.01, - "minimal_roi": { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - }, - "stoploss": -0.10 - } - - @pytest.fixture def update(): _update = Update(0) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 9748deeea..1c72a70ba 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -78,10 +78,10 @@ def test_parse_args_backtesting(mocker): def test_parse_args_backtesting_invalid(): with pytest.raises(SystemExit, match=r'2'): - parse_args(['--ticker-interval']) + parse_args(['backtesting --ticker-interval']) with pytest.raises(SystemExit, match=r'2'): - parse_args(['--ticker-interval', 'abc']) + parse_args(['backtesting --ticker-interval', 'abc']) def test_parse_args_backtesting_custom(mocker): @@ -99,6 +99,19 @@ def test_parse_args_backtesting_custom(mocker): assert call_args.ticker_interval == 1 +def test_parse_args_hyperopt(mocker): + hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) + args = parse_args(['hyperopt']) + assert args is None + assert hyperopt_mock.call_count == 1 + + call_args = hyperopt_mock.call_args[0][0] + assert call_args.config == 'config.json' + assert call_args.loglevel == 20 + assert call_args.subparser == 'hyperopt' + assert call_args.func is not None + + def test_load_config(default_conf, mocker): file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( read_data=json.dumps(default_conf) diff --git a/freqtrade/tests/test_optimize_backtesting.py b/freqtrade/tests/test_optimize_backtesting.py index 36f4cd144..7986f6d35 100644 --- a/freqtrade/tests/test_optimize_backtesting.py +++ b/freqtrade/tests/test_optimize_backtesting.py @@ -1,18 +1,16 @@ # pragma pylint: disable=missing-docstring,W0212 -from freqtrade import exchange +from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex -from freqtrade.optimize.backtesting import backtest, preprocess -from freqtrade.tests import load_backtesting_data +from freqtrade.optimize.backtesting import backtest -def test_backtest(backtest_conf, mocker): - mocker.patch.dict('freqtrade.main._CONF', backtest_conf) +def test_backtest(default_conf, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) exchange._API = Bittrex({'key': '', 'secret': ''}) - data = load_backtesting_data(ticker_interval=5, pairs=['BTC_ETH']) - results = backtest(backtest_conf, preprocess(data), 10, True) + data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH']) + results = backtest(default_conf, optimize.preprocess(data), 10, True) num_resutls = len(results) assert num_resutls > 0 - diff --git a/freqtrade/tests/test_optimize_hyperopt.py b/freqtrade/tests/test_optimize_hyperopt.py new file mode 100644 index 000000000..a8bfe7dd4 --- /dev/null +++ b/freqtrade/tests/test_optimize_hyperopt.py @@ -0,0 +1,6 @@ +# pragma pylint: disable=missing-docstring,W0212 + + +def test_optimizer(default_conf, mocker): + # TODO: implement test + pass