[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',
help='Specify hyperopt class name which will be used by the bot.',
metavar='NAME',
required=False,
),
"hyperopt_path": Arg(
'--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.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)

View File

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

View File

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

View File

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

View File

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