diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/decimalspace.py new file mode 100644 index 000000000..f5370b6d6 --- /dev/null +++ b/freqtrade/optimize/decimalspace.py @@ -0,0 +1,32 @@ +import numpy as np +from skopt.space import Integer + + +class SKDecimal(Integer): + + def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, + name=None, dtype=np.int64): + self.decimals = decimals + _low = int(low * pow(10, self.decimals)) + _high = int(high * pow(10, self.decimals)) + self.low_orig = low + self.high_orig = high + + super().__init__(_low, _high, prior, base, transform, name, dtype) + + def __repr__(self): + return "Decimal(low={}, high={}, decimals={}, prior='{}', transform='{}')".format( + self.low_orig, self.high_orig, self.decimals, self.prior, self.transform_) + + def __contains__(self, point): + if isinstance(point, list): + point = np.array(point) + return self.low_orig <= point <= self.high_orig + + def transform(self, Xt): + aa = [int(x * pow(10, self.decimals)) for x in Xt] + return super().transform(aa) + + def inverse_transform(self, Xt): + res = super().inverse_transform(Xt) + return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index c4d6f1581..f86204406 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -27,7 +27,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -37,7 +37,7 @@ class HyperOptAuto(IHyperOpt): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: # noinspection PyProtectedMember - attr._set_value(params[attr_name]) + attr.value = params[attr_name] return self.strategy.populate_sell_trend(dataframe, metadata) return populate_sell_trend diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 709179997..3fedda974 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,6 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical + from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.exceptions import OperationalException @@ -24,9 +25,8 @@ class BaseParameter(ABC): category: Optional[str] default: Any value: Any - opt_range: Sequence[Any] - def __init__(self, *, opt_range: Sequence[Any], default: Any, space: Optional[str] = None, + def __init__(self, *, default: Any, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ Initialize hyperopt-optimizable parameter. @@ -43,7 +43,6 @@ class BaseParameter(ABC): self.category = space self._space_params = kwargs self.value = default - self.opt_range = opt_range self.optimize = optimize self.load = load @@ -51,24 +50,51 @@ class BaseParameter(ABC): return f'{self.__class__.__name__}({self.value})' @abstractmethod - def get_space(self, name: str) -> Union['Integer', 'Real', 'Categorical']: + def get_space(self, name: str) -> Union['Integer', 'Real', 'SKDecimal', 'Categorical']: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ - def _set_value(self, value: Any): + +class NumericParameter(BaseParameter): + """ Internal parameter used for Numeric purposes """ + float_or_int = Union[int, float] + default: float_or_int + value: float_or_int + + def __init__(self, low: Union[float_or_int, Sequence[float_or_int]], + high: Optional[float_or_int] = None, *, default: float_or_int, + space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: A numerical value. + Initialize hyperopt-optimizable numeric parameter. + Cannot be instantiated, but provides the validation for other numeric parameters + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none of entire range is passed first parameter. + :param default: A default value. + :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if + parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param optimize: Include parameter in hyperopt optimizations. + :param load: Load parameter value from {space}_params. + :param kwargs: Extra parameters to skopt.space.*. """ - self.value = value + if high is not None and isinstance(low, Sequence): + raise OperationalException(f'{self.__class__.__name__} space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') + self.low, self.high = low + else: + self.low = low + self.high = high + + super().__init__(default=default, space=space, optimize=optimize, + load=load, **kwargs) -class IntParameter(BaseParameter): +class IntParameter(NumericParameter): default: int value: int - opt_range: Sequence[int] def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: int, space: Optional[str] = None, optimize: bool = True, load: bool = True, **kwargs): @@ -84,15 +110,8 @@ class IntParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException('IntParameter space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') - opt_range = low - else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Integer': @@ -100,13 +119,12 @@ class IntParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Integer(*self.opt_range, name=name, **self._space_params) + return Integer(low=self.low, high=self.high, name=name, **self._space_params) -class RealParameter(BaseParameter): +class RealParameter(NumericParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, space: Optional[str] = None, optimize: bool = True, @@ -123,15 +141,7 @@ class RealParameter(BaseParameter): :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is not None and isinstance(low, Sequence): - raise OperationalException(f'{self.__class__.__name__} space invalid.') - if high is None or isinstance(low, Sequence): - if not isinstance(low, Sequence) or len(low) != 2: - raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') - opt_range = low - else: - opt_range = [low, high] - super().__init__(opt_range=opt_range, default=default, space=space, optimize=optimize, + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Real': @@ -139,13 +149,12 @@ class RealParameter(BaseParameter): Create skopt optimization space. :param name: A name of parameter field. """ - return Real(*self.opt_range, name=name, **self._space_params) + return Real(low=self.low, high=self.high, name=name, **self._space_params) -class DecimalParameter(RealParameter): +class DecimalParameter(NumericParameter): default: float value: float - opt_range: Sequence[float] def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, default: float, decimals: int = 3, space: Optional[str] = None, @@ -161,29 +170,21 @@ class DecimalParameter(RealParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. - :param kwargs: Extra parameters to skopt.space.Real. + :param kwargs: Extra parameters to skopt.space.Integer. """ self._decimals = decimals default = round(default, self._decimals) + super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> 'Integer': + def get_space(self, name: str) -> 'SKDecimal': """ Create skopt optimization space. :param name: A name of parameter field. """ - low = int(self.opt_range[0] * pow(10, self._decimals)) - high = int(self.opt_range[1] * pow(10, self._decimals)) - return Integer(low, high, name=name, **self._space_params) - - def _set_value(self, value: int): - """ - Update current value. Used by hyperopt functions for the purpose where optimization and - value spaces differ. - :param value: An integer value. - """ - self.value = round(value * pow(0.1, self._decimals), self._decimals) + return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name, + **self._space_params) class CategoricalParameter(BaseParameter): @@ -208,7 +209,8 @@ class CategoricalParameter(BaseParameter): if len(categories) < 2: raise OperationalException( 'CategoricalParameter space must be [a, b, ...] (at least two parameters)') - super().__init__(opt_range=categories, default=default, space=space, optimize=optimize, + self.opt_range = categories + super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) def get_space(self, name: str) -> 'Categorical': diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c13da0d76..129fe53d9 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,6 +15,7 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException +from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -1104,3 +1105,20 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir) -> None: assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) hyperopt.start() + + +def test_SKDecimal(): + space = SKDecimal(1, 2, decimals=2) + assert 1.5 in space + assert 2.5 not in space + assert space.low == 100 + assert space.high == 200 + + assert space.inverse_transform([200]) == [2.0] + assert space.inverse_transform([100]) == [1.0] + assert space.inverse_transform([150, 160]) == [1.5, 1.6] + + assert space.transform([1.5]) == [150] + assert space.transform([2.0]) == [200] + assert space.transform([1.0]) == [100] + assert space.transform([1.5, 1.6]) == [150, 160] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3bfa691b4..0ee80e0c5 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -596,8 +596,6 @@ def test_hyperopt_parameters(): fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') assert isinstance(fltpar.get_space(''), Integer) assert fltpar.value == 1 - fltpar._set_value(2222) - assert fltpar.value == 2.222 catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], default='buy_macd', space='buy')