[SQUASH] Address PR comments.
* Split Parameter into IntParameter/FloatParameter/CategoricalParameter. * Rename IHyperStrategy to HyperStrategyMixin and use it as mixin. * --hyperopt parameter is now optional if strategy uses HyperStrategyMixin. * Use OperationalException() instead of asserts.
This commit is contained in:
		| @@ -195,6 +195,7 @@ AVAILABLE_CLI_OPTIONS = { | |||||||
|         '--hyperopt', |         '--hyperopt', | ||||||
|         help='Specify hyperopt class name which will be used by the bot.', |         help='Specify hyperopt class name which will be used by the bot.', | ||||||
|         metavar='NAME', |         metavar='NAME', | ||||||
|  |         required=False, | ||||||
|     ), |     ), | ||||||
|     "hyperopt_path": Arg( |     "hyperopt_path": Arg( | ||||||
|         '--hyperopt-path', |         '--hyperopt-path', | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ from pandas import DataFrame | |||||||
| from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN | from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN | ||||||
| from freqtrade.data.converter import trim_dataframe | from freqtrade.data.converter import trim_dataframe | ||||||
| from freqtrade.data.history import get_timerange | from freqtrade.data.history import get_timerange | ||||||
|  | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.misc import file_dump_json, plural | from freqtrade.misc import file_dump_json, plural | ||||||
| from freqtrade.optimize.backtesting import Backtesting | from freqtrade.optimize.backtesting import Backtesting | ||||||
| # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules | # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules | ||||||
| @@ -68,7 +69,11 @@ class Hyperopt: | |||||||
|  |  | ||||||
|         self.backtesting = Backtesting(self.config) |         self.backtesting = Backtesting(self.config) | ||||||
|  |  | ||||||
|         if self.config['hyperopt'] == 'HyperOptAuto': |         if not self.config.get('hyperopt'): | ||||||
|  |             if not getattr(self.backtesting.strategy, 'HYPER_STRATEGY', False): | ||||||
|  |                 raise OperationalException('Strategy is not auto-hyperoptable. Specify --hyperopt ' | ||||||
|  |                                            'parameter or add HyperStrategyMixin mixin to your ' | ||||||
|  |                                            'strategy class.') | ||||||
|             self.custom_hyperopt = HyperOptAuto(self.config) |             self.custom_hyperopt = HyperOptAuto(self.config) | ||||||
|         else: |         else: | ||||||
|             self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) |             self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """ | """ | ||||||
| HyperOptAuto class. | HyperOptAuto class. | ||||||
| This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement | This module implements a convenience auto-hyperopt class, which can be used together with strategies | ||||||
| IHyperStrategy interface. |  that implement IHyperStrategy interface. | ||||||
| """ | """ | ||||||
| from typing import Any, Callable, Dict, List | from typing import Any, Callable, Dict, List | ||||||
| from pandas import DataFrame | from pandas import DataFrame | ||||||
| @@ -13,26 +13,31 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt | |||||||
| # noinspection PyUnresolvedReferences | # noinspection PyUnresolvedReferences | ||||||
| class HyperOptAuto(IHyperOpt): | class HyperOptAuto(IHyperOpt): | ||||||
|     """ |     """ | ||||||
|     This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. Most of the time |     This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. | ||||||
|     Strategy.HyperOpt class would only implement indicator_space and sell_indicator_space methods, but other hyperopt |      Most of the time Strategy.HyperOpt class would only implement indicator_space and | ||||||
|     methods can be overridden as well. |      sell_indicator_space methods, but other hyperopt methods can be overridden as well. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: |     def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: | ||||||
|         assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' |         if not getattr(self.strategy, 'HYPER_STRATEGY', False): | ||||||
|  |             raise OperationalException('Strategy must inherit from IHyperStrategy.') | ||||||
|  |  | ||||||
|         def populate_buy_trend(dataframe: DataFrame, metadata: dict): |         def populate_buy_trend(dataframe: DataFrame, metadata: dict): | ||||||
|             for attr_name, attr in self.strategy.enumerate_parameters('buy'): |             for attr_name, attr in self.strategy.enumerate_parameters('buy'): | ||||||
|                 attr.value = params[attr_name] |                 attr.value = params[attr_name] | ||||||
|             return self.strategy.populate_buy_trend(dataframe, metadata) |             return self.strategy.populate_buy_trend(dataframe, metadata) | ||||||
|  |  | ||||||
|         return populate_buy_trend |         return populate_buy_trend | ||||||
|  |  | ||||||
|     def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: |     def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: | ||||||
|         assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' |         if not getattr(self.strategy, 'HYPER_STRATEGY', False): | ||||||
|  |             raise OperationalException('Strategy must inherit from IHyperStrategy.') | ||||||
|  |  | ||||||
|         def populate_buy_trend(dataframe: DataFrame, metadata: dict): |         def populate_buy_trend(dataframe: DataFrame, metadata: dict): | ||||||
|             for attr_name, attr in self.strategy.enumerate_parameters('sell'): |             for attr_name, attr in self.strategy.enumerate_parameters('sell'): | ||||||
|                 attr.value = params[attr_name] |                 attr.value = params[attr_name] | ||||||
|             return self.strategy.populate_sell_trend(dataframe, metadata) |             return self.strategy.populate_sell_trend(dataframe, metadata) | ||||||
|  |  | ||||||
|         return populate_buy_trend |         return populate_buy_trend | ||||||
|  |  | ||||||
|     def _get_func(self, name) -> Callable: |     def _get_func(self, name) -> Callable: | ||||||
| @@ -49,7 +54,8 @@ class HyperOptAuto(IHyperOpt): | |||||||
|             return default_func |             return default_func | ||||||
|  |  | ||||||
|     def _generate_indicator_space(self, category): |     def _generate_indicator_space(self, category): | ||||||
|         assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.' |         if not getattr(self.strategy, 'HYPER_STRATEGY', False): | ||||||
|  |             raise OperationalException('Strategy must inherit from IHyperStrategy.') | ||||||
|  |  | ||||||
|         for attr_name, attr in self.strategy.enumerate_parameters(category): |         for attr_name, attr in self.strategy.enumerate_parameters(category): | ||||||
|             yield attr.get_space(attr_name) |             yield attr.get_space(attr_name) | ||||||
|   | |||||||
| @@ -5,15 +5,14 @@ This module defines the interface to apply for hyperopt | |||||||
| import logging | import logging | ||||||
| import math | import math | ||||||
| from abc import ABC | from abc import ABC | ||||||
| from typing import Any, Callable, Dict, List | from typing import Any, Callable, Dict, List, Union | ||||||
|  |  | ||||||
| from skopt.space import Categorical, Dimension, Integer, Real | from skopt.space import Categorical, Dimension, Integer, Real | ||||||
|  |  | ||||||
| from freqtrade.exceptions import OperationalException | from freqtrade.exceptions import OperationalException | ||||||
| from freqtrade.exchange import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.misc import round_dict | from freqtrade.misc import round_dict | ||||||
| from freqtrade.strategy import IStrategy | from freqtrade.strategy import IStrategy, HyperStrategyMixin | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -35,7 +34,7 @@ class IHyperOpt(ABC): | |||||||
|     """ |     """ | ||||||
|     ticker_interval: str  # DEPRECATED |     ticker_interval: str  # DEPRECATED | ||||||
|     timeframe: str |     timeframe: str | ||||||
|     strategy: IStrategy |     strategy: Union[IStrategy, HyperStrategyMixin] | ||||||
|  |  | ||||||
|     def __init__(self, config: dict) -> None: |     def __init__(self, config: dict) -> None: | ||||||
|         self.config = config |         self.config = config | ||||||
|   | |||||||
| @@ -2,5 +2,6 @@ | |||||||
| from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, | from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, | ||||||
|                                 timeframe_to_prev_date, timeframe_to_seconds) |                                 timeframe_to_prev_date, timeframe_to_seconds) | ||||||
| from freqtrade.strategy.interface import IStrategy | from freqtrade.strategy.interface import IStrategy | ||||||
| from freqtrade.strategy.hyper import IHyperStrategy, Parameter | from freqtrade.strategy.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ | ||||||
|  |     CategoricalParameter | ||||||
| from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open | from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open | ||||||
|   | |||||||
| @@ -2,83 +2,158 @@ | |||||||
| IHyperStrategy interface, hyperoptable Parameter class. | IHyperStrategy interface, hyperoptable Parameter class. | ||||||
| This module defines a base class for auto-hyperoptable strategies. | This module defines a base class for auto-hyperoptable strategies. | ||||||
| """ | """ | ||||||
| from abc import ABC | from typing import Iterator, Tuple, Any, Optional, Sequence | ||||||
| from typing import Union, List, Iterator, Tuple |  | ||||||
|  |  | ||||||
| from skopt.space import Integer, Real, Categorical | from skopt.space import Integer, Real, Categorical | ||||||
|  |  | ||||||
| from freqtrade.strategy.interface import IStrategy | from freqtrade.exceptions import OperationalException | ||||||
|  |  | ||||||
|  |  | ||||||
| class Parameter(object): | class BaseParameter(object): | ||||||
|     """ |     """ | ||||||
|     Defines a parameter that can be optimized by hyperopt. |     Defines a parameter that can be optimized by hyperopt. | ||||||
|     """ |     """ | ||||||
|     default: Union[int, float, str, bool] |     category: Optional[str] | ||||||
|     space: List[Union[int, float, str, bool]] |     default: Any | ||||||
|     category: str |     value: Any | ||||||
|  |     space: Sequence[Any] | ||||||
|  |  | ||||||
|     def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, |     def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[str] = None, | ||||||
|                  category: str = None, **kwargs): |                  **kwargs): | ||||||
|         """ |         """ | ||||||
|         Initialize hyperopt-optimizable parameter. |         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 |         :param category: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if parameter field | ||||||
|          name is prefixed with 'buy_' or 'sell_'. |          name is prefixed with 'buy_' or 'sell_'. | ||||||
|         :param kwargs: Extra parameters to skopt.space.(Integer|Real|Categorical). |         :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.' |         if 'name' in kwargs: | ||||||
|         self.value = default |             raise OperationalException( | ||||||
|         self.space = space |                 'Name is determined by parameter field name and can not be specified manually.') | ||||||
|         self.category = category |         self.category = category | ||||||
|         self._space_params = kwargs |         self._space_params = kwargs | ||||||
|         if default is None: |         self.value = default | ||||||
|             assert len(space) > 0 |         self.space = space | ||||||
|             self.value = space[0] |  | ||||||
|  |  | ||||||
|     def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]: |     def __repr__(self): | ||||||
|  |         return f'{self.__class__.__name__}({self.value})' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class IntParameter(BaseParameter): | ||||||
|  |     default: int | ||||||
|  |     value: int | ||||||
|  |     space: Sequence[int] | ||||||
|  |  | ||||||
|  |     def __init__(self, *, space: Sequence[int], default: int, category: Optional[str] = None, | ||||||
|  |                  **kwargs): | ||||||
|  |         """ | ||||||
|  |         Initialize hyperopt-optimizable parameter. | ||||||
|  |         :param space: Optimization space, [min, max]. | ||||||
|  |         :param default: A default value. | ||||||
|  |         :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. | ||||||
|  |         """ | ||||||
|  |         if len(space) != 2: | ||||||
|  |             raise OperationalException('IntParameter space must be [min, max]') | ||||||
|  |         super().__init__(space=space, default=default, category=category, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_space(self, name: str) -> Integer: | ||||||
|         """ |         """ | ||||||
|         Create skopt optimization space. |         Create skopt optimization space. | ||||||
|         :param name: A name of parameter field. |         :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) |         return Integer(*self.space, name=name, **self._space_params) | ||||||
|         if isinstance(self.value, float): |  | ||||||
|             assert len(self.space) == 2 |  | ||||||
|  | class FloatParameter(BaseParameter): | ||||||
|  |     default: float | ||||||
|  |     value: float | ||||||
|  |     space: Sequence[float] | ||||||
|  |  | ||||||
|  |     def __init__(self, *, space: Sequence[float], default: float, category: Optional[str] = None, | ||||||
|  |                  **kwargs): | ||||||
|  |         """ | ||||||
|  |         Initialize hyperopt-optimizable parameter. | ||||||
|  |         :param space: Optimization space, [min, max]. | ||||||
|  |         :param default: A default value. | ||||||
|  |         :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.Real. | ||||||
|  |         """ | ||||||
|  |         if len(space) != 2: | ||||||
|  |             raise OperationalException('IntParameter space must be [min, max]') | ||||||
|  |         super().__init__(space=space, default=default, category=category, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_space(self, name: str) -> Real: | ||||||
|  |         """ | ||||||
|  |         Create skopt optimization space. | ||||||
|  |         :param name: A name of parameter field. | ||||||
|  |         """ | ||||||
|         return Real(*self.space, name=name, **self._space_params) |         return Real(*self.space, name=name, **self._space_params) | ||||||
|  |  | ||||||
|         assert len(self.space) > 0 |  | ||||||
|  | class CategoricalParameter(BaseParameter): | ||||||
|  |     default: Any | ||||||
|  |     value: Any | ||||||
|  |     space: Sequence[Any] | ||||||
|  |  | ||||||
|  |     def __init__(self, *, space: Sequence[Any], default: Optional[Any] = None, | ||||||
|  |                  category: Optional[str] = None, | ||||||
|  |                  **kwargs): | ||||||
|  |         """ | ||||||
|  |         Initialize hyperopt-optimizable parameter. | ||||||
|  |         :param space: Optimization space, [a, b, ...]. | ||||||
|  |         :param default: A default value. If not specified, first item from specified space will be | ||||||
|  |          used. | ||||||
|  |         :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.Categorical. | ||||||
|  |         """ | ||||||
|  |         if len(space) < 2: | ||||||
|  |             raise OperationalException( | ||||||
|  |                 'IntParameter space must be [a, b, ...] (at least two parameters)') | ||||||
|  |         super().__init__(space=space, default=default, category=category, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_space(self, name: str) -> Categorical: | ||||||
|  |         """ | ||||||
|  |         Create skopt optimization space. | ||||||
|  |         :param name: A name of parameter field. | ||||||
|  |         """ | ||||||
|         return Categorical(self.space, name=name, **self._space_params) |         return Categorical(self.space, name=name, **self._space_params) | ||||||
|  |  | ||||||
|  |  | ||||||
| class IHyperStrategy(IStrategy, ABC): | class HyperStrategyMixin(object): | ||||||
|     """ |     """ | ||||||
|     A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic. |     A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell | ||||||
|  |      strategy logic. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     def __init__(self, config): |     # Hint that class can be used with HyperOptAuto. | ||||||
|         super().__init__(config) |     HYPER_STRATEGY = 1 | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         """ | ||||||
|  |         Initialize hyperoptable strategy mixin. | ||||||
|  |         :param config: | ||||||
|  |         """ | ||||||
|         self._load_params(getattr(self, 'buy_params', None)) |         self._load_params(getattr(self, 'buy_params', None)) | ||||||
|         self._load_params(getattr(self, 'sell_params', None)) |         self._load_params(getattr(self, 'sell_params', None)) | ||||||
|  |  | ||||||
|     def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Parameter]]: |     def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: | ||||||
|         """ |         """ | ||||||
|         Find all optimizeable parameters and return (name, attr) iterator. |         Find all optimizeable parameters and return (name, attr) iterator. | ||||||
|         :param category: |         :param category: | ||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|         assert category in ('buy', 'sell', None) |         if category not in ('buy', 'sell', None): | ||||||
|  |             raise OperationalException('Category must be one of: "buy", "sell", None.') | ||||||
|         for attr_name in dir(self): |         for attr_name in dir(self): | ||||||
|             if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary. |             if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary. | ||||||
|                 attr = getattr(self, attr_name) |                 attr = getattr(self, attr_name) | ||||||
|                 if isinstance(attr, Parameter): |                 if issubclass(attr.__class__, BaseParameter): | ||||||
|                     if category is None or category == attr.category or attr_name.startswith(category + '_'): |                     if category is None or category == attr.category or \ | ||||||
|  |                        attr_name.startswith(category + '_'): | ||||||
|                         yield attr_name, attr |                         yield attr_name, attr | ||||||
|  |  | ||||||
|     def _load_params(self, params: dict) -> None: |     def _load_params(self, params: dict) -> None: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user