[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', | ||||
|         help='Specify hyperopt class name which will be used by the bot.', | ||||
|         metavar='NAME', | ||||
|         required=False, | ||||
|     ), | ||||
|     "hyperopt_path": Arg( | ||||
|         '--hyperopt-path', | ||||
|   | ||||
| @@ -23,6 +23,7 @@ from pandas import DataFrame | ||||
| from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN | ||||
| from freqtrade.data.converter import trim_dataframe | ||||
| from freqtrade.data.history import get_timerange | ||||
| from freqtrade.exceptions import OperationalException | ||||
| 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 | ||||
| @@ -68,7 +69,11 @@ class Hyperopt: | ||||
|  | ||||
|         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) | ||||
|         else: | ||||
|             self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| """ | ||||
| HyperOptAuto class. | ||||
| This module implements a convenience auto-hyperopt class, which can be used together with strategies that implement | ||||
| IHyperStrategy interface. | ||||
| 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 | ||||
| @@ -13,26 +13,31 @@ 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. | ||||
|     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.' | ||||
|         if not getattr(self.strategy, 'HYPER_STRATEGY', False): | ||||
|             raise OperationalException('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.' | ||||
|         if not getattr(self.strategy, 'HYPER_STRATEGY', False): | ||||
|             raise OperationalException('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: | ||||
| @@ -49,7 +54,8 @@ class HyperOptAuto(IHyperOpt): | ||||
|             return default_func | ||||
|  | ||||
|     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): | ||||
|             yield attr.get_space(attr_name) | ||||
|   | ||||
| @@ -5,15 +5,14 @@ This module defines the interface to apply for hyperopt | ||||
| import logging | ||||
| import math | ||||
| 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 freqtrade.exceptions import OperationalException | ||||
| from freqtrade.exchange import timeframe_to_minutes | ||||
| from freqtrade.misc import round_dict | ||||
| from freqtrade.strategy import IStrategy | ||||
|  | ||||
| from freqtrade.strategy import IStrategy, HyperStrategyMixin | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -35,7 +34,7 @@ class IHyperOpt(ABC): | ||||
|     """ | ||||
|     ticker_interval: str  # DEPRECATED | ||||
|     timeframe: str | ||||
|     strategy: IStrategy | ||||
|     strategy: Union[IStrategy, HyperStrategyMixin] | ||||
|  | ||||
|     def __init__(self, config: dict) -> None: | ||||
|         self.config = config | ||||
|   | ||||
| @@ -2,5 +2,6 @@ | ||||
| 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.hyper import HyperStrategyMixin, IntParameter, FloatParameter,\ | ||||
|     CategoricalParameter | ||||
| from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open | ||||
|   | ||||
| @@ -2,83 +2,158 @@ | ||||
| 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 typing import Iterator, Tuple, Any, Optional, Sequence | ||||
|  | ||||
| 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. | ||||
|     """ | ||||
|     default: Union[int, float, str, bool] | ||||
|     space: List[Union[int, float, str, bool]] | ||||
|     category: str | ||||
|     category: Optional[str] | ||||
|     default: Any | ||||
|     value: Any | ||||
|     space: Sequence[Any] | ||||
|  | ||||
|     def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None, | ||||
|                  category: str = None, **kwargs): | ||||
|     def __init__(self, *, space: Sequence[Any], default: Any, category: Optional[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 | ||||
|         if 'name' in kwargs: | ||||
|             raise OperationalException( | ||||
|                 'Name is determined by parameter field name and can not be specified manually.') | ||||
|         self.category = category | ||||
|         self._space_params = kwargs | ||||
|         if default is None: | ||||
|             assert len(space) > 0 | ||||
|             self.value = space[0] | ||||
|         self.value = default | ||||
|         self.space = space | ||||
|  | ||||
|     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. | ||||
|         :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) | ||||
|         return Integer(*self.space, name=name, **self._space_params) | ||||
|  | ||||
|         assert len(self.space) > 0 | ||||
|  | ||||
| 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) | ||||
|  | ||||
|  | ||||
| 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) | ||||
|  | ||||
|  | ||||
| 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): | ||||
|         super().__init__(config) | ||||
|     # Hint that class can be used with HyperOptAuto. | ||||
|     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, '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. | ||||
|         :param category: | ||||
|         :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): | ||||
|             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 + '_'): | ||||
|                 if issubclass(attr.__class__, BaseParameter): | ||||
|                     if category is None or category == attr.category or \ | ||||
|                        attr_name.startswith(category + '_'): | ||||
|                         yield attr_name, attr | ||||
|  | ||||
|     def _load_params(self, params: dict) -> None: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user