diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 0e3cf7eac..ed6f2d6f7 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -26,7 +26,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('buy'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_buy_trend(dataframe, metadata) return populate_buy_trend @@ -35,7 +36,8 @@ class HyperOptAuto(IHyperOpt): def populate_buy_trend(dataframe: DataFrame, metadata: dict): for attr_name, attr in self.strategy.enumerate_parameters('sell'): if attr.optimize: - attr.value = params[attr_name] + # noinspection PyProtectedMember + attr._set_value(params[attr_name]) return self.strategy.populate_sell_trend(dataframe, metadata) return populate_buy_trend diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index bc0c45f7c..bd49165df 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,6 +1,7 @@ # flake8: noqa: F401 from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.strategy.hyper import CategoricalParameter, FloatParameter, IntParameter +from freqtrade.strategy.hyper import (CategoricalParameter, DecimalParameter, IntParameter, + RealParameter) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index a6603ecbf..e58aac273 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -56,6 +56,14 @@ class BaseParameter(ABC): Get-space - will be used by Hyperopt to get the hyperopt Space """ + def _set_value(self, value: Any): + """ + Update current value. Used by hyperopt functions for the purpose where optimization and + value spaces differ. + :param value: A numerical value. + """ + self.value = value + class IntParameter(BaseParameter): default: int @@ -65,7 +73,7 @@ class IntParameter(BaseParameter): 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): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable integer parameter. :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. @@ -95,16 +103,16 @@ class IntParameter(BaseParameter): return Integer(*self.opt_range, name=name, **self._space_params) -class FloatParameter(BaseParameter): +class RealParameter(BaseParameter): 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, load: bool = True, **kwargs): + default: float, space: Optional[str] = None, optimize: bool = True, + load: bool = True, **kwargs): """ - Initialize hyperopt-optimizable parameter. + Initialize hyperopt-optimizable floating point parameter with unlimited precision. :param low: Lower end (inclusive) of optimization space or [low, high]. :param high: Upper end (inclusive) of optimization space. Must be none if entire range is passed first parameter. @@ -116,10 +124,10 @@ class FloatParameter(BaseParameter): :param kwargs: Extra parameters to skopt.space.Real. """ if high is not None and isinstance(low, Sequence): - raise OperationalException('FloatParameter space invalid.') + 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('FloatParameter space must be [low, high]') + raise OperationalException(f'{self.__class__.__name__} space must be [low, high]') opt_range = low else: opt_range = [low, high] @@ -134,6 +142,50 @@ class FloatParameter(BaseParameter): return Real(*self.opt_range, name=name, **self._space_params) +class DecimalParameter(RealParameter): + 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, + optimize: bool = True, load: bool = True, **kwargs): + """ + Initialize hyperopt-optimizable decimal parameter with a limited precision. + :param low: Lower end (inclusive) of optimization space or [low, high]. + :param high: Upper end (inclusive) of optimization space. + Must be none if entire range is passed first parameter. + :param default: A default value. + :param decimals: A number of decimals after floating point to be included in testing. + :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.Real. + """ + 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': + """ + 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) + + class CategoricalParameter(BaseParameter): default: Any value: Any diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index a08293058..cc4734e13 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import FloatParameter, IntParameter, IStrategy +from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy, RealParameter class HyperoptableStrategy(IStrategy): @@ -60,9 +60,10 @@ class HyperoptableStrategy(IStrategy): } buy_rsi = IntParameter([0, 50], default=30, space='buy') - buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') - sell_minusdi = FloatParameter(low=0, high=1, default=0.5, space='sell', load=False) + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) def informative_pairs(self): """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4d93f7049..71f877cc3 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,8 +13,8 @@ from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, FloatParameter, - IntParameter) +from freqtrade.strategy.hyper import (BaseParameter, CategoricalParameter, DecimalParameter, + IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -564,14 +564,20 @@ def test_hyperopt_parameters(): with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): IntParameter(low=0, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): - FloatParameter(low=0, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space must be.*"): + RealParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space must be.*"): + DecimalParameter(low=0, default=5, space='buy') with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): IntParameter([0, 10], high=7, default=5, space='buy') - with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): - FloatParameter([0, 10], high=7, default=5, space='buy') + with pytest.raises(OperationalException, match=r"RealParameter space invalid\."): + RealParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"DecimalParameter space invalid\."): + DecimalParameter([0, 10], high=7, default=5, space='buy') with pytest.raises(OperationalException, match=r"CategoricalParameter space must.*"): CategoricalParameter(['aa'], default='aa', space='buy') @@ -583,10 +589,16 @@ def test_hyperopt_parameters(): assert intpar.value == 1 assert isinstance(intpar.get_space(''), Integer) - fltpar = FloatParameter(low=0.0, high=5.5, default=1.0, space='buy') + fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space='buy') assert isinstance(fltpar.get_space(''), Real) assert fltpar.value == 1 + 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') assert isinstance(catpar.get_space(''), Categorical)