Merge pull request #2492 from hroff-1902/hyperopt-trailing-space

Add trailing stoploss hyperspace
This commit is contained in:
hroff-1902
2019-12-03 00:23:14 +03:00
committed by GitHub
6 changed files with 231 additions and 63 deletions

View File

@@ -184,11 +184,10 @@ AVAILABLE_CLI_OPTIONS = {
),
"spaces": Arg(
'--spaces',
help='Specify which parameters to hyperopt. Space-separated list. '
'Default: `%(default)s`.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
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

@@ -170,45 +170,43 @@ class Hyperopt:
best_result = results[0]
params = best_result['params']
log_str = self.format_results_logstring(best_result)
print(f"\nBest result:\n\n{log_str}\n")
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')})
if self.has_space('sell'):
result_dict['params'].update({p.name: params.get(p.name)
for p in self.hyperopt_space('sell')})
if self.has_space('roi'):
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
self._params_update_for_json(result_dict, params, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
self._params_pretty_print(params, 'buy', "Buy hyperspace params:")
self._params_pretty_print(params, 'sell', "Sell hyperspace params:")
self._params_pretty_print(params, 'roi', "ROI table:")
self._params_pretty_print(params, 'stoploss', "Stoploss:")
self._params_pretty_print(params, 'trailing', "Trailing stop:")
def _params_update_for_json(self, result_dict, params, space: str):
if self.has_space(space):
space_params = self.space_params(params, space)
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
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 space_params.items()
)
if self.has_space('stoploss'):
result_dict['stoploss'] = params.get('stoploss')
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)
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)
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)}")
else: # 'stoploss', 'trailing'
result_dict.update(space_params)
def _params_pretty_print(self, params, space: str, header: str):
if self.has_space(space):
space_params = self.space_params(params, space, 5)
print(header)
pprint(space_params, indent=4)
def is_best(self, results) -> bool:
return results['loss'] < self.current_best_loss
@@ -250,9 +248,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]:
"""
@@ -262,20 +264,37 @@ 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:
if space == 'roi':
d = self.custom_hyperopt.generate_roi_table(params)
else:
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.
@@ -298,6 +317,14 @@ 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,27 @@ 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 [
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
# trailing_stop is set False.
# This parameter is included into the hyperspace dimensions rather than assigning
# it explicitly in the code in order to have it printed in the results along with
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, 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

@@ -233,6 +233,27 @@ class AdvancedSampleHyperOpt(IHyperOpt):
Real(-0.5, -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 [
# It was decided to always set trailing_stop is to True if the 'trailing' hyperspace
# is used. Otherwise hyperopt will vary other parameters that won't have effect if
# trailing_stop is set False.
# This parameter is included into the hyperspace dimensions rather than assigning
# it explicitly in the code in order to have it printed in the results along with
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, name='trailing_stop_positive'),
Real(0.01, 0.1, name='trailing_stop_positive_offset'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators.