From bb89e44e19825dc9b9c1bca393ced8a646800cf5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 24 Mar 2021 10:32:34 +0200 Subject: [PATCH] [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. --- freqtrade/commands/cli_options.py | 1 + freqtrade/optimize/hyperopt.py | 7 +- freqtrade/optimize/hyperopt_auto.py | 22 ++-- freqtrade/optimize/hyperopt_interface.py | 7 +- freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/hyper.py | 149 +++++++++++++++++------ 6 files changed, 138 insertions(+), 51 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 15c13cec9..8e9f0c994 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -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', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8dd8f01ac..b5ee1da1a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -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) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 788bd5a79..fdc4da14c 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -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) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 561fb8e11..2dd8500a6 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -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 diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 80a7c00de..e395be106 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -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 diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 41bb836d4..8de986950 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -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: