[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:
Rokas Kupstys 2021-03-24 10:32:34 +02:00
parent 0a205f52b0
commit bb89e44e19
6 changed files with 138 additions and 51 deletions

View File

@ -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',

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 Integer(*self.space, name=name, **self._space_params)
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
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) 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: