From 2595479e4348c56ffae8d0ee65d1a58ae3264bee Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 30 Mar 2025 21:11:26 +0300 Subject: [PATCH] change CategoricalParameter and IntParameter in parameters.py to use optuna.distributions CategoricalDistribution and IntDistribution instead of skopt --- .../optimize/hyperopt/hyperopt_optimizer.py | 10 +++- freqtrade/strategy/parameters.py | 55 +++++++++++++++---- tests/strategy/test_interface.py | 9 +-- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 8e1daade8..d60a272ea 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -39,6 +39,7 @@ with warnings.catch_warnings(): from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.optimize.space.decimalspace import SKDecimal + from freqtrade.strategy.parameters import ft_CategoricalDistribution, ft_IntDistribution logger = logging.getLogger(__name__) @@ -420,6 +421,11 @@ class HyperOptimizer: o_dimensions[original_dim.name] = optuna.distributions.CategoricalDistribution( list(original_dim.bounds) ) + # for preparing to remove old skopt spaces + elif isinstance( + original_dim, ft_CategoricalDistribution + ) or isinstance(original_dim, ft_IntDistribution): + o_dimensions[original_dim.name] = original_dim else: raise Exception(f"Unknown search space {original_dim} / {type(original_dim)}") # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") @@ -429,7 +435,9 @@ class HyperOptimizer: self, random_state: int, ): - o_sampler = self.custom_hyperopt.generate_estimator(dimensions=self.dimensions) + o_sampler = self.custom_hyperopt.generate_estimator( + dimensions=self.dimensions, random_state=random_state + ) self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) # for save/restore diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 282e630d0..17f6da006 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -14,7 +14,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): - from skopt.space import Categorical, Integer, Real + from optuna.distributions import CategoricalDistribution, IntDistribution + from skopt.space import Integer, Real # Categorical from freqtrade.optimize.space import SKDecimal @@ -24,6 +25,25 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) +class ft_CategoricalDistribution(CategoricalDistribution): + name: str + def __init__( + self, + categories: Sequence[Any], + **kwargs, + ): + return super().__init__(categories) + +class ft_IntDistribution(IntDistribution): + name: str + def __init__( + self, + low: int, + high: int, + **kwargs, + ): + return super().__init__(low, high, **kwargs) + class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. @@ -51,7 +71,8 @@ class BaseParameter(ABC): name 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.(Integer|Real|Categorical). + :param kwargs: Extra parameters to optuna.distributions. + (IntDistribution|Real|CategoricalDistribution). """ if "name" in kwargs: raise OperationalException( @@ -67,7 +88,9 @@ class BaseParameter(ABC): return f"{self.__class__.__name__}({self.value})" @abstractmethod - def get_space(self, name: str) -> Union["Integer", "Real", "SKDecimal", "Categorical"]: + def get_space(self, name: str) -> Union[ + "ft_IntDistribution", "Real", "SKDecimal", "ft_CategoricalDistribution" + ]: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ @@ -151,7 +174,7 @@ class IntParameter(NumericParameter): 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.Integer. + :param kwargs: Extra parameters to optuna.distributions.IntDistribution. """ super().__init__( @@ -160,10 +183,15 @@ class IntParameter(NumericParameter): def get_space(self, name: str) -> "Integer": """ - Create skopt optimization space. + Create optuna distribution space. :param name: A name of parameter field. """ - return Integer(low=self.low, high=self.high, name=name, **self._space_params) + # return Integer(low=self.low, high=self.high, name=name, **self._space_params) + result = ft_IntDistribution( + self.low, self.high, **self._space_params + ) + result.name = name + return result @property def range(self): @@ -174,7 +202,7 @@ class IntParameter(NumericParameter): calculating 100ds of indicators. """ if self.can_optimize(): - # Scikit-optimize ranges are "inclusive", while python's "range" is exclusive + # optuna distributions ranges are "inclusive", while python's "range" is exclusive return range(self.low, self.high + 1) else: return range(self.value, self.value + 1) @@ -305,7 +333,7 @@ class CategoricalParameter(BaseParameter): name 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.Categorical. + :param kwargs: Extra parameters to optuna.distributions.CategoricalDistribution. """ if len(categories) < 2: raise OperationalException( @@ -314,12 +342,15 @@ class CategoricalParameter(BaseParameter): self.opt_range = categories super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> "Categorical": + def get_space(self, name: str) -> "ft_CategoricalDistribution": """ - Create skopt optimization space. + Create optuna distribution space. :param name: A name of parameter field. """ - return Categorical(self.opt_range, name=name, **self._space_params) + # Categorical(self.opt_range, name=name, **self._space_params) + result = ft_CategoricalDistribution(self.opt_range) + result.name = name + return result @property def range(self): @@ -355,7 +386,7 @@ class BooleanParameter(CategoricalParameter): name 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.Categorical. + :param kwargs: Extra parameters to optuna.distributions.CategoricalDistribution. """ categories = [True, False] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 647484535..8cbe40d13 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -895,7 +895,8 @@ def test_is_informative_pairs_callback(default_conf): def test_hyperopt_parameters(): HyperoptStateContainer.set_state(HyperoptState.INDICATORS) - from skopt.space import Categorical, Integer, Real + from optuna.distributions import CategoricalDistribution, IntDistribution + from skopt.space import Real with pytest.raises(OperationalException, match=r"Name is determined.*"): IntParameter(low=0, high=5, default=1, name="hello") @@ -926,7 +927,7 @@ def test_hyperopt_parameters(): intpar = IntParameter(low=0, high=5, default=1, space="buy") assert intpar.value == 1 - assert isinstance(intpar.get_space(""), Integer) + assert isinstance(intpar.get_space(""), IntDistribution) assert isinstance(intpar.range, range) assert len(list(intpar.range)) == 1 # Range contains ONLY the default / value. @@ -955,7 +956,7 @@ def test_hyperopt_parameters(): ["buy_rsi", "buy_macd", "buy_none"], default="buy_macd", space="buy" ) assert catpar.value == "buy_macd" - assert isinstance(catpar.get_space(""), Categorical) + assert isinstance(catpar.get_space(""), CategoricalDistribution) assert isinstance(catpar.range, list) assert len(list(catpar.range)) == 1 # Range contains ONLY the default / value. @@ -966,7 +967,7 @@ def test_hyperopt_parameters(): boolpar = BooleanParameter(default=True, space="buy") assert boolpar.value is True - assert isinstance(boolpar.get_space(""), Categorical) + assert isinstance(boolpar.get_space(""), CategoricalDistribution) assert isinstance(boolpar.range, list) assert len(list(boolpar.range)) == 1