Add trailing stoploss hyperspace

This commit is contained in:
hroff-1902 2019-11-08 01:55:14 +03:00
parent ad2289c34c
commit f90676cfc5
4 changed files with 110 additions and 34 deletions

View File

@ -174,12 +174,11 @@ AVAILABLE_CLI_OPTIONS = {
default=constants.HYPEROPT_EPOCH, default=constants.HYPEROPT_EPOCH,
), ),
"spaces": Arg( "spaces": Arg(
'-s', '--spaces', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list. ' help='Specify which parameters to hyperopt. Space-separated list.',
'Default: `%(default)s`.', choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'default'],
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
nargs='+', nargs='+',
default='all', default='default',
), ),
"print_all": Arg( "print_all": Arg(
'--print-all', '--print-all',

View File

@ -149,7 +149,7 @@ class Hyperopt:
self.trials_file.unlink() self.trials_file.unlink()
return trials return trials
def log_trials_result(self) -> None: def log_trials_result(self) -> None: # noqa: C901
""" """
Display Best hyperopt result Display Best hyperopt result
""" """
@ -161,14 +161,16 @@ class Hyperopt:
if self.config.get('print_json'): if self.config.get('print_json'):
result_dict: Dict = {} result_dict: Dict = {}
if self.has_space('buy') or self.has_space('sell'): if self.has_space('buy') or self.has_space('sell'):
result_dict['params'] = {} result_dict['params'] = {}
if self.has_space('buy'): if self.has_space('buy'):
result_dict['params'].update({p.name: params.get(p.name) result_dict['params'].update(self.space_params(params, 'buy'))
for p in self.hyperopt_space('buy')})
if self.has_space('sell'): if self.has_space('sell'):
result_dict['params'].update({p.name: params.get(p.name) result_dict['params'].update(self.space_params(params, 'sell'))
for p in self.hyperopt_space('sell')})
if self.has_space('roi'): if self.has_space('roi'):
# Convert keys in min_roi dict to strings because # Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys... # rapidjson cannot dump dicts with integer keys...
@ -177,25 +179,35 @@ class Hyperopt:
result_dict['minimal_roi'] = OrderedDict( result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items() (str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items()
) )
if self.has_space('stoploss'): 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)) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else: else:
if self.has_space('buy'): if self.has_space('buy'):
print('Buy hyperspace params:') print('Buy hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')}, pprint(self.space_params(params, 'buy', 5), indent=4)
indent=4)
if self.has_space('sell'): if self.has_space('sell'):
print('Sell hyperspace params:') print('Sell hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')}, pprint(self.space_params(params, 'sell', 5), indent=4)
indent=4)
if self.has_space('roi'): if self.has_space('roi'):
print("ROI table:") print("ROI table:")
# Round printed values to 5 digits after the decimal point # Round printed values to 5 digits after the decimal point
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4) pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
if self.has_space('stoploss'): if self.has_space('stoploss'):
# Also round to 5 digits after the decimal point print(f"Stoploss:")
print(f"Stoploss: {round(params.get('stoploss'), 5)}") 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: def log_results(self, results) -> None:
""" """
@ -233,9 +245,13 @@ class Hyperopt:
def has_space(self, space: str) -> bool: 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
""" """
# 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']) 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]: def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
""" """
@ -245,20 +261,34 @@ class Hyperopt:
for all hyperspaces used. for all hyperspaces used.
""" """
spaces: List[Dimension] = [] spaces: List[Dimension] = []
if space == 'buy' or (space is None and self.has_space('buy')): if space == 'buy' or (space is None and self.has_space('buy')):
logger.debug("Hyperopt has 'buy' space") logger.debug("Hyperopt has 'buy' space")
spaces += self.custom_hyperopt.indicator_space() spaces += self.custom_hyperopt.indicator_space()
if space == 'sell' or (space is None and self.has_space('sell')): if space == 'sell' or (space is None and self.has_space('sell')):
logger.debug("Hyperopt has 'sell' space") logger.debug("Hyperopt has 'sell' space")
spaces += self.custom_hyperopt.sell_indicator_space() spaces += self.custom_hyperopt.sell_indicator_space()
if space == 'roi' or (space is None and self.has_space('roi')): if space == 'roi' or (space is None and self.has_space('roi')):
logger.debug("Hyperopt has 'roi' space") logger.debug("Hyperopt has 'roi' space")
spaces += self.custom_hyperopt.roi_space() spaces += self.custom_hyperopt.roi_space()
if space == 'stoploss' or (space is None and self.has_space('stoploss')): if space == 'stoploss' or (space is None and self.has_space('stoploss')):
logger.debug("Hyperopt has 'stoploss' space") logger.debug("Hyperopt has 'stoploss' space")
spaces += self.custom_hyperopt.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 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: def generate_optimizer(self, _params: Dict, iteration=None) -> Dict:
""" """
Used Optimize function. Called once per epoch to optimize whatever is configured. Used Optimize function. Called once per epoch to optimize whatever is configured.
@ -281,6 +311,15 @@ class Hyperopt:
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params['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) processed = load(self.tickerdata_pickle)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)

View File

@ -8,7 +8,7 @@ import math
from abc import ABC from abc import ABC
from typing import Dict, Any, Callable, List 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 import OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -174,6 +174,20 @@ class IHyperOpt(ABC):
Real(-0.35, -0.02, name='stoploss'), 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 # This is needed for proper unpickling the class attribute ticker_interval
# which is set to the actual value by the resolver. # which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python? # Why do I still need such shamanic mantras in modern python?

View File

@ -26,7 +26,7 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def hyperopt(default_conf, mocker): def hyperopt(default_conf, mocker):
default_conf.update({'spaces': ['all']}) default_conf.update({'spaces': ['default']})
patch_exchange(mocker) patch_exchange(mocker)
return Hyperopt(default_conf) return Hyperopt(default_conf)
@ -108,7 +108,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--epochs', '1000', '--epochs', '1000',
'--spaces', 'all', '--spaces', 'default',
'--print-all' '--print-all'
] ]
@ -414,7 +414,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'all', 'spaces': 'default',
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
@ -463,14 +463,38 @@ def test_format_results(hyperopt):
assert result.find('Total profit 1.00000000 EUR') assert result.find('Total profit 1.00000000 EUR')
def test_has_space(hyperopt): @pytest.mark.parametrize("spaces, expected_results", [
hyperopt.config.update({'spaces': ['buy', 'roi']}) (['buy'],
assert hyperopt.has_space('roi') {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False}),
assert hyperopt.has_space('buy') (['sell'],
assert not hyperopt.has_space('stoploss') {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False}),
(['roi'],
hyperopt.config.update({'spaces': ['all']}) {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False}),
assert hyperopt.has_space('buy') (['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: 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: def test_generate_optimizer(mocker, default_conf) -> None:
default_conf.update({'config': 'config.json.example'}) default_conf.update({'config': 'config.json.example'})
default_conf.update({'timerange': None}) default_conf.update({'timerange': None})
default_conf.update({'spaces': 'all'}) default_conf.update({'spaces': 'default'})
default_conf.update({'hyperopt_min_trades': 1}) default_conf.update({'hyperopt_min_trades': 1})
trades = [ trades = [
@ -584,7 +608,7 @@ def test_clean_hyperopt(mocker, default_conf, caplog):
default_conf.update({'config': 'config.json.example', default_conf.update({'config': 'config.json.example',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'all', 'spaces': 'default',
'hyperopt_jobs': 1, 'hyperopt_jobs': 1,
}) })
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) 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', default_conf.update({'config': 'config.json.example',
'epochs': 1, 'epochs': 1,
'timerange': None, 'timerange': None,
'spaces': 'all', 'spaces': 'default',
'hyperopt_jobs': 1, 'hyperopt_jobs': 1,
'hyperopt_continue': True 'hyperopt_continue': True
}) })