Optional support for defining hyperopt parameters in a strategy file and reusing common hyperopt/strategy parts.

This commit is contained in:
Rokas Kupstys 2021-03-23 10:02:32 +02:00
parent 8da7d5c009
commit 0a205f52b0
4 changed files with 183 additions and 2 deletions

View File

@ -26,6 +26,7 @@ from freqtrade.data.history import get_timerange
from freqtrade.misc import file_dump_json, plural from freqtrade.misc import file_dump_json, plural
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.hyperopt_tools import HyperoptTools
@ -67,8 +68,11 @@ class Hyperopt:
self.backtesting = Backtesting(self.config) self.backtesting = Backtesting(self.config)
if self.config['hyperopt'] == 'HyperOptAuto':
self.custom_hyperopt = HyperOptAuto(self.config)
else:
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
self.custom_hyperopt.__class__.strategy = self.backtesting.strategy self.custom_hyperopt.strategy = self.backtesting.strategy
self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config)
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function

View File

@ -0,0 +1,83 @@
"""
HyperOptAuto class.
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
from skopt.space import Categorical, Dimension, Integer, Real # noqa
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.
"""
def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable:
assert hasattr(self.strategy, 'enumerate_parameters'), '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.'
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:
"""
Return a function defined in Strategy.HyperOpt class, or one defined in super() class.
:param name: function name.
:return: a requested function.
"""
hyperopt_cls = getattr(self.strategy, 'HyperOpt')
default_func = getattr(super(), name)
if hyperopt_cls:
return getattr(hyperopt_cls, name, default_func)
else:
return default_func
def _generate_indicator_space(self, category):
assert hasattr(self.strategy, 'enumerate_parameters'), 'Strategy must inherit from IHyperStrategy.'
for attr_name, attr in self.strategy.enumerate_parameters(category):
yield attr.get_space(attr_name)
def _get_indicator_space(self, category, fallback_method_name):
indicator_space = list(self._generate_indicator_space(category))
if len(indicator_space) > 0:
return indicator_space
else:
return self._get_func(fallback_method_name)()
def indicator_space(self) -> List[Dimension]:
return self._get_indicator_space('buy', 'indicator_space')
def sell_indicator_space(self) -> List[Dimension]:
return self._get_indicator_space('sell', 'sell_indicator_space')
def generate_roi_table(self, params: Dict) -> Dict[int, float]:
return self._get_func('generate_roi_table')(params)
def roi_space(self) -> List[Dimension]:
return self._get_func('roi_space')()
def stoploss_space(self) -> List[Dimension]:
return self._get_func('stoploss_space')()
def generate_trailing_params(self, params: Dict) -> Dict:
return self._get_func('generate_trailing_params')(params)
def trailing_space(self) -> List[Dimension]:
return self._get_func('trailing_space')()

View File

@ -2,4 +2,5 @@
from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
timeframe_to_prev_date, timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.hyper import IHyperStrategy, Parameter
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open

View File

@ -0,0 +1,93 @@
"""
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 skopt.space import Integer, Real, Categorical
from freqtrade.strategy.interface import IStrategy
class Parameter(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
def __init__(self, *, space: List[Union[int, float, str, bool]], default: Union[int, float, str, bool] = None,
category: 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
self.category = category
self._space_params = kwargs
if default is None:
assert len(space) > 0
self.value = space[0]
def get_space(self, name: str) -> Union[Integer, Real, Categorical, None]:
"""
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)
assert len(self.space) > 0
return Categorical(self.space, name=name, **self._space_params)
class IHyperStrategy(IStrategy, ABC):
"""
A helper base class which allows HyperOptAuto class to reuse implementations of of buy/sell strategy logic.
"""
def __init__(self, config):
super().__init__(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]]:
"""
Find all optimizeable parameters and return (name, attr) iterator.
:param category:
:return:
"""
assert category in ('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 + '_'):
yield attr_name, attr
def _load_params(self, params: dict) -> None:
"""
Set optimizeable parameter values.
:param params: Dictionary with new parameter values.
"""
if not params:
return
for attr_name, attr in self.enumerate_parameters():
if attr_name in params:
attr.value = params[attr_name]