Merge pull request #4703 from freqtrade/decimal_space

Add SKDecimal Space
This commit is contained in:
Matthias 2021-04-12 19:18:10 +02:00 committed by GitHub
commit cb60db01b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 53 deletions

View File

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

View File

@ -27,7 +27,7 @@ class HyperOptAuto(IHyperOpt):
for attr_name, attr in self.strategy.enumerate_parameters('buy'): for attr_name, attr in self.strategy.enumerate_parameters('buy'):
if attr.optimize: if attr.optimize:
# noinspection PyProtectedMember # noinspection PyProtectedMember
attr._set_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
@ -37,7 +37,7 @@ class HyperOptAuto(IHyperOpt):
for attr_name, attr in self.strategy.enumerate_parameters('sell'): for attr_name, attr in self.strategy.enumerate_parameters('sell'):
if attr.optimize: if attr.optimize:
# noinspection PyProtectedMember # noinspection PyProtectedMember
attr._set_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_sell_trend return populate_sell_trend

View File

@ -10,6 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union
with suppress(ImportError): with suppress(ImportError):
from skopt.space import Integer, Real, Categorical from skopt.space import Integer, Real, Categorical
from freqtrade.optimize.decimalspace import SKDecimal
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@ -24,9 +25,8 @@ class BaseParameter(ABC):
category: Optional[str] category: Optional[str]
default: Any default: Any
value: 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): optimize: bool = True, load: bool = True, **kwargs):
""" """
Initialize hyperopt-optimizable parameter. Initialize hyperopt-optimizable parameter.
@ -43,7 +43,6 @@ class BaseParameter(ABC):
self.category = space self.category = space
self._space_params = kwargs self._space_params = kwargs
self.value = default self.value = default
self.opt_range = opt_range
self.optimize = optimize self.optimize = optimize
self.load = load self.load = load
@ -51,24 +50,51 @@ class BaseParameter(ABC):
return f'{self.__class__.__name__}({self.value})' return f'{self.__class__.__name__}({self.value})'
@abstractmethod @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 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 Initialize hyperopt-optimizable numeric parameter.
value spaces differ. Cannot be instantiated, but provides the validation for other numeric parameters
:param value: A numerical value. :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 default: int
value: int value: int
opt_range: Sequence[int]
def __init__(self, low: Union[int, Sequence[int]], high: Optional[int] = None, *, default: 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): 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 load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Integer. :param kwargs: Extra parameters to skopt.space.Integer.
""" """
if high is not None and isinstance(low, Sequence):
raise OperationalException('IntParameter space invalid.') super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
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,
load=load, **kwargs) load=load, **kwargs)
def get_space(self, name: str) -> 'Integer': def get_space(self, name: str) -> 'Integer':
@ -100,13 +119,12 @@ class IntParameter(BaseParameter):
Create skopt optimization space. Create skopt optimization space.
:param name: A name of parameter field. :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 default: float
value: float value: float
opt_range: Sequence[float]
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
default: float, space: Optional[str] = None, optimize: bool = True, 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 load: Load parameter value from {space}_params.
:param kwargs: Extra parameters to skopt.space.Real. :param kwargs: Extra parameters to skopt.space.Real.
""" """
if high is not None and isinstance(low, Sequence): super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
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,
load=load, **kwargs) load=load, **kwargs)
def get_space(self, name: str) -> 'Real': def get_space(self, name: str) -> 'Real':
@ -139,13 +149,12 @@ class RealParameter(BaseParameter):
Create skopt optimization space. Create skopt optimization space.
:param name: A name of parameter field. :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 default: float
value: float value: float
opt_range: Sequence[float]
def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *, def __init__(self, low: Union[float, Sequence[float]], high: Optional[float] = None, *,
default: float, decimals: int = 3, space: Optional[str] = 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_'. parameter fieldname is prefixed with 'buy_' or 'sell_'.
:param optimize: Include parameter in hyperopt optimizations. :param optimize: Include parameter in hyperopt optimizations.
:param load: Load parameter value from {space}_params. :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 self._decimals = decimals
default = round(default, self._decimals) default = round(default, self._decimals)
super().__init__(low=low, high=high, default=default, space=space, optimize=optimize, super().__init__(low=low, high=high, default=default, space=space, optimize=optimize,
load=load, **kwargs) load=load, **kwargs)
def get_space(self, name: str) -> 'Integer': def get_space(self, name: str) -> 'SKDecimal':
""" """
Create skopt optimization space. Create skopt optimization space.
:param name: A name of parameter field. :param name: A name of parameter field.
""" """
low = int(self.opt_range[0] * pow(10, self._decimals)) return SKDecimal(low=self.low, high=self.high, decimals=self._decimals, name=name,
high = int(self.opt_range[1] * pow(10, self._decimals)) **self._space_params)
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)
class CategoricalParameter(BaseParameter): class CategoricalParameter(BaseParameter):
@ -208,7 +209,8 @@ class CategoricalParameter(BaseParameter):
if len(categories) < 2: if len(categories) < 2:
raise OperationalException( raise OperationalException(
'CategoricalParameter space must be [a, b, ...] (at least two parameters)') '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) load=load, **kwargs)
def get_space(self, name: str) -> 'Categorical': def get_space(self, name: str) -> 'Categorical':

View File

@ -15,6 +15,7 @@ from filelock import Timeout
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.decimalspace import SKDecimal
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_tools import HyperoptTools 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) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
hyperopt.start() 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]

View File

@ -596,8 +596,6 @@ def test_hyperopt_parameters():
fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy') fltpar = DecimalParameter(low=0.0, high=5.5, default=1.0004, decimals=3, space='buy')
assert isinstance(fltpar.get_space(''), Integer) assert isinstance(fltpar.get_space(''), Integer)
assert fltpar.value == 1 assert fltpar.value == 1
fltpar._set_value(2222)
assert fltpar.value == 2.222
catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'], catpar = CategoricalParameter(['buy_rsi', 'buy_macd', 'buy_none'],
default='buy_macd', space='buy') default='buy_macd', space='buy')