diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 03f34a511..8dd8f01ac 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -26,6 +26,7 @@ from freqtrade.data.history import get_timerange from freqtrade.misc import file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -67,8 +68,11 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperopt.__class__.strategy = self.backtesting.strategy + if self.config['hyperopt'] == 'HyperOptAuto': + self.custom_hyperopt = HyperOptAuto(self.config) + else: + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) + self.custom_hyperopt.strategy = self.backtesting.strategy self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py new file mode 100644 index 000000000..788bd5a79 --- /dev/null +++ b/freqtrade/optimize/hyperopt_auto.py @@ -0,0 +1,83 @@ +""" +HyperOptAuto class. +This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement +IHyperStrategy interface. +""" +from typing import Any, Callable, Dict, List +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer, Real # noqa + +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +# noinspection PyUnresolvedReferences +class HyperOptAuto(IHyperOpt): + """ + This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time + Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt + methods can be overridden as well. + """ + def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('buy'): + attr.value = params[attr_name] + return self.strategy.populate_buy_trend(dataframe, metadata) + return populate_buy_trend + + def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + def populate_buy_trend(dataframe: DataFrame, metadata: dict): + for attr_name, attr in self.strategy.enumerate_parameters('sell'): + attr.value = params[attr_name] + return self.strategy.populate_sell_trend(dataframe, metadata) + return populate_buy_trend + + def _get_func(self, name) -> Callable: + """ + Return a function defined in Strategy.HyperOpt class, or one defined in super() class. + :param name: function name. + :return: a requested function. + """ + hyperopt_cls = getattr(self.strategy, 'HyperOpt') + default_func = getattr(super(), name) + if hyperopt_cls: + return getattr(hyperopt_cls, name, default_func) + else: + return default_func + + def _generate_indicator_space(self, category): + assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' + + for attr_name, attr in self.strategy.enumerate_parameters(category): + yield attr.get_space(attr_name) + + def _get_indicator_space(self, category, fallback_method_name): + indicator_space = list(self._generate_indicator_space(category)) + if len(indicator_space) > 0: + return indicator_space + else: + return self._get_func(fallback_method_name)() + + def indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('buy', 'indicator_space') + + def sell_indicator_space(self) -> List[Dimension]: + return self._get_indicator_space('sell', 'sell_indicator_space') + + def generate_roi_table(self, params: Dict) -> Dict[int, float]: + return self._get_func('generate_roi_table')(params) + + def roi_space(self) -> List[Dimension]: + return self._get_func('roi_space')() + + def stoploss_space(self) -> List[Dimension]: + return self._get_func('stoploss_space')() + + def generate_trailing_params(self, params: Dict) -> Dict: + return self._get_func('generate_trailing_params')(params) + + def trailing_space(self) -> List[Dimension]: + return self._get_func('trailing_space')() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 85148b6ea..80a7c00de 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.hyper import IHyperStrategy, Parameter from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py new file mode 100644 index 000000000..41bb836d4 --- /dev/null +++ b/freqtrade/strategy/hyper.py @@ -0,0 +1,93 @@ +""" +IHyperStrategy interface, hyperoptable Parameter class. +This module defines a base class for auto-hyperoptable strategies. +""" +from abc import ABC +from typing import Union, List, Iterator, Tuple + +from skopt.space import Integer, Real, Categorical + +from freqtrade.strategy.interface import IStrategy + + +class Parameter(object): + """ + Defines a parameter that can be optimized by hyperopt. + """ + default: Union[int, float, str, bool] + space: List[Union[int, float, str, bool]] + category: str + + def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, + category: str = None, **kwargs): + """ + Initialize hyperopt-optimizable parameter. + :param space: Optimization space. [min, max] for ints and floats or a list of strings for categorial parameters. + :param default: A default value. Required for ints and floats, optional for categorial parameters (first item + from the space will be used). Type of default value determines skopt space used for optimization. + :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field + name is prefixed with 'buy_' or 'sell_'. + :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). + """ + assert 'name' not in kwargs, 'Name is determined by parameter field name and can not be specified manually.' + self.value = default + self.space = space + self.category = category + self._space_params = kwargs + if default is None: + assert len(space) > 0 + self.value = space[0] + + def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: + """ + Create skopt optimization space. + :param name: A name of parameter field. + :return: skopt space of this parameter, or None if parameter is not optimizable (i.e. space is set to None) + """ + if not self.space: + return None + if isinstance(self.value, int): + assert len(self.space) == 2 + return Integer(*self.space, name=name, **self._space_params) + if isinstance(self.value, float): + assert len(self.space) == 2 + return Real(*self.space, name=name, **self._space_params) + + assert len(self.space) > 0 + return Categorical(self.space, name=name, **self._space_params) + + +class IHyperStrategy(IStrategy, ABC): + """ + A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. + """ + + def __init__(self, config): + super().__init__(config) + self._load_params(getattr(self, 'buy_params', None)) + self._load_params(getattr(self, 'sell_params', None)) + + def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: + """ + Find all optimizeable parameters and return (name, attr) iterator. + :param category: + :return: + """ + assert category in ('buy', 'sell', None) + for attr_name in dir(self): + if not attr_name.startswith('__'): # Ignore internals, not strictly necessary. + attr = getattr(self, attr_name) + if isinstance(attr, Parameter): + if category is None or category == attr.category or attr_name.startswith(category + '_'): + yield attr_name, attr + + def _load_params(self, params: dict) -> None: + """ + Set optimizeable parameter values. + :param params: Dictionary with new parameter values. + """ + if not params: + return + for attr_name, attr in self.enumerate_parameters(): + if attr_name in params: + attr.value = params[attr_name]