From f90676cfc5a249bf957b62a509ee07f6c6731e11 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 8 Nov 2019 01:55:14 +0300 Subject: [PATCH] Add trailing stoploss hyperspace --- freqtrade/configuration/cli_options.py | 9 ++-- freqtrade/optimize/hyperopt.py | 67 +++++++++++++++++++----- freqtrade/optimize/hyperopt_interface.py | 16 +++++- tests/optimize/test_hyperopt.py | 52 +++++++++++++----- 4 files changed, 110 insertions(+), 34 deletions(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 697e048db..e2b786ac7 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -174,12 +174,11 @@ AVAILABLE_CLI_OPTIONS = { default=constants.HYPEROPT_EPOCH, ), "spaces": Arg( - '-s', '--spaces', - help='Specify which parameters to hyperopt. Space-separated list. ' - 'Default: `%(default)s`.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss'], + '--spaces', + help='Specify which parameters to hyperopt. Space-separated list.', + choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'], nargs='+', - default='all', + default='default', ), "print_all": Arg( '--print-all', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6ea2f5133..c5a003bd6 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -149,7 +149,7 @@ class Hyperopt: self.trials_file.unlink() return trials - def log_trials_result(self) -> None: + def log_trials_result(self) -> None: # noqa: C901 """ Display Best hyperopt result """ @@ -161,14 +161,16 @@ class Hyperopt: if self.config.get('print_json'): result_dict: Dict = {} + if self.has_space('buy') or self.has_space('sell'): result_dict['params'] = {} + if self.has_space('buy'): - result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('buy')}) + result_dict['params'].update(self.space_params(params, 'buy')) + if self.has_space('sell'): - result_dict['params'].update({p.name: params.get(p.name) - for p in self.hyperopt_space('sell')}) + result_dict['params'].update(self.space_params(params, 'sell')) + if self.has_space('roi'): # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... @@ -177,25 +179,35 @@ class Hyperopt: result_dict['minimal_roi'] = OrderedDict( (str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() ) + if self.has_space('stoploss'): - result_dict['stoploss'] = params.get('stoploss') + result_dict.update(self.space_params(params, 'stoploss')) + + if self.has_space('trailing'): + result_dict.update(self.space_params(params, 'trailing')) + print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) else: if self.has_space('buy'): print('Buy hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, - indent=4) + pprint(self.space_params(params, 'buy', 5), indent=4) + if self.has_space('sell'): print('Sell hyperspace params:') - pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, - indent=4) + pprint(self.space_params(params, 'sell', 5), indent=4) + if self.has_space('roi'): print("ROI table:") # Round printed values to 5 digits after the decimal point pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4) + if self.has_space('stoploss'): - # Also round to 5 digits after the decimal point - print(f"Stoploss: {round(params.get('stoploss'), 5)}") + print(f"Stoploss:") + pprint(self.space_params(params, 'stoploss', 5), indent=4) + + if self.has_space('trailing'): + print('Trailing stop:') + pprint(self.space_params(params, 'trailing', 5), indent=4) def log_results(self, results) -> None: """ @@ -233,9 +245,13 @@ class Hyperopt: def has_space(self, space: str) -> bool: """ - Tell if a space value is contained in the configuration + Tell if the space value is contained in the configuration """ - return any(s in self.config['spaces'] for s in [space, 'all']) + # The 'trailing' space is not included in the 'default' set of spaces + if space == 'trailing': + return any(s in self.config['spaces'] for s in [space, 'all']) + else: + return any(s in self.config['spaces'] for s in [space, 'all', 'default']) def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]: """ @@ -245,20 +261,34 @@ class Hyperopt: for all hyperspaces used. """ spaces: List[Dimension] = [] + if space == 'buy' or (space is None and self.has_space('buy')): logger.debug("Hyperopt has 'buy' space") spaces += self.custom_hyperopt.indicator_space() + if space == 'sell' or (space is None and self.has_space('sell')): logger.debug("Hyperopt has 'sell' space") spaces += self.custom_hyperopt.sell_indicator_space() + if space == 'roi' or (space is None and self.has_space('roi')): logger.debug("Hyperopt has 'roi' space") spaces += self.custom_hyperopt.roi_space() + if space == 'stoploss' or (space is None and self.has_space('stoploss')): logger.debug("Hyperopt has 'stoploss' space") spaces += self.custom_hyperopt.stoploss_space() + + if space == 'trailing' or (space is None and self.has_space('trailing')): + logger.debug("Hyperopt has 'trailing' space") + spaces += self.custom_hyperopt.trailing_space() + return spaces + def space_params(self, params, space: str, r: int = None) -> Dict: + d = {p.name: params.get(p.name) for p in self.hyperopt_space(space)} + # Round floats to `r` digits after the decimal point if requested + return round_dict(d, r) if r else d + def generate_optimizer(self, _params: Dict, iteration=None) -> Dict: """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -281,6 +311,15 @@ class Hyperopt: if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params['stoploss'] + if self.has_space('trailing'): + self.backtesting.strategy.trailing_stop = params['trailing_stop'] + self.backtesting.strategy.trailing_stop_positive = \ + params['trailing_stop_positive'] + self.backtesting.strategy.trailing_stop_positive_offset = \ + params['trailing_stop_positive_offset'] + self.backtesting.strategy.trailing_only_offset_is_reached = \ + params['trailing_only_offset_is_reached'] + processed = load(self.tickerdata_pickle) min_date, max_date = get_timeframe(processed) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 142f305df..e8f16d572 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -8,7 +8,7 @@ import math from abc import ABC from typing import Dict, Any, Callable, List -from skopt.space import Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real from freqtrade import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -174,6 +174,20 @@ class IHyperOpt(ABC): Real(-0.35, -0.02, name='stoploss'), ] + @staticmethod + def trailing_space() -> List[Dimension]: + """ + Create a trailing stoploss space. + + You may override it in your custom Hyperopt class. + """ + return [ + Categorical([True, False], name='trailing_stop'), + Real(-0.35, -0.02, name='trailing_stop_positive'), + Real(0.01, 0.1, name='trailing_stop_positive_offset'), + Categorical([True, False], name='trailing_only_offset_is_reached'), + ] + # This is needed for proper unpickling the class attribute ticker_interval # which is set to the actual value by the resolver. # Why do I still need such shamanic mantras in modern python? diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 23d8a887c..e247d0134 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -26,7 +26,7 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, @pytest.fixture(scope='function') def hyperopt(default_conf, mocker): - default_conf.update({'spaces': ['all']}) + default_conf.update({'spaces': ['default']}) patch_exchange(mocker) return Hyperopt(default_conf) @@ -108,7 +108,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--enable-position-stacking', '--disable-max-market-positions', '--epochs', '1000', - '--spaces', 'all', + '--spaces', 'default', '--print-all' ] @@ -414,7 +414,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: default_conf.update({'config': 'config.json.example', 'epochs': 1, 'timerange': None, - 'spaces': 'all', + 'spaces': 'default', 'hyperopt_jobs': 1, }) hyperopt = Hyperopt(default_conf) @@ -463,14 +463,38 @@ def test_format_results(hyperopt): assert result.find('Total profit 1.00000000 EUR') -def test_has_space(hyperopt): - hyperopt.config.update({'spaces': ['buy', 'roi']}) - assert hyperopt.has_space('roi') - assert hyperopt.has_space('buy') - assert not hyperopt.has_space('stoploss') - - hyperopt.config.update({'spaces': ['all']}) - assert hyperopt.has_space('buy') +@pytest.mark.parametrize("spaces, expected_results", [ + (['buy'], + {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}), + (['sell'], + {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}), + (['roi'], + {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}), + (['stoploss'], + {'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False}), + (['trailing'], + {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True}), + (['buy', 'sell', 'roi', 'stoploss'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), + (['buy', 'sell', 'roi', 'stoploss', 'trailing'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), + (['buy', 'roi'], + {'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}), + (['all'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), + (['default'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), + (['default', 'trailing'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), + (['all', 'buy'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True}), + (['default', 'buy'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False}), +]) +def test_has_space(hyperopt, spaces, expected_results): + for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']: + hyperopt.config.update({'spaces': spaces}) + assert hyperopt.has_space(s) == expected_results[s] def test_populate_indicators(hyperopt, testdatadir) -> None: @@ -517,7 +541,7 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: def test_generate_optimizer(mocker, default_conf) -> None: default_conf.update({'config': 'config.json.example'}) default_conf.update({'timerange': None}) - default_conf.update({'spaces': 'all'}) + default_conf.update({'spaces': 'default'}) default_conf.update({'hyperopt_min_trades': 1}) trades = [ @@ -584,7 +608,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog): default_conf.update({'config': 'config.json.example', 'epochs': 1, 'timerange': None, - 'spaces': 'all', + 'spaces': 'default', 'hyperopt_jobs': 1, }) mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) @@ -600,7 +624,7 @@ def test_continue_hyperopt(mocker, default_conf, caplog): default_conf.update({'config': 'config.json.example', 'epochs': 1, 'timerange': None, - 'spaces': 'all', + 'spaces': 'default', 'hyperopt_jobs': 1, 'hyperopt_continue': True })