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,
),
"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',

View File

@ -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)

View File

@ -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?

View File

@ -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
})