From 62f05964b4c0b04297ea93e2cd9d2b56e5173226 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Tue, 25 Mar 2025 14:06:35 +0200 Subject: [PATCH 01/64] change optimizer to optuna --- freqtrade/optimize/hyperopt/hyperopt.py | 38 ++++--- .../optimize/hyperopt/hyperopt_interface.py | 9 +- .../optimize/hyperopt/hyperopt_optimizer.py | 105 +++++++++++++----- requirements-hyperopt.txt | 3 + tests/optimize/test_hyperopt.py | 17 +-- 5 files changed, 119 insertions(+), 53 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index ef71860a5..9eb5c9531 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -37,7 +37,7 @@ INITIAL_POINTS = 30 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption -SKOPT_MODEL_QUEUE_SIZE = 10 +# SKOPT_MODEL_QUEUE_SIZE = 10 log_queue: Any @@ -165,7 +165,13 @@ class Hyperopt: def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 - def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]: + def get_optuna_asked_points(self, n_points: int, dimensions: dict) -> list[Any]: + asked: list[list[Any]] = [] + for i in range(n_points): + asked.append(self.opt.ask(dimensions)) + return asked + + def get_asked_points(self, n_points: int, dimensions: dict) -> tuple[list[list[Any]], list[bool]]: """ Enforce points returned from `self.opt.ask` have not been already evaluated @@ -191,7 +197,8 @@ class Hyperopt: while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} - asked = unique_list(self.opt.ask(n_points=n_points * 5 if i > 0 else n_points)) + asked = unique_list(self.get_optuna_asked_points(n_points=n_points * 5 if i > 0 else n_points, + dimensions=dimensions)) is_random = [False for _ in range(len(asked))] else: asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) @@ -199,10 +206,10 @@ class Hyperopt: is_random_non_tried += [ rand for x, rand in zip(asked, is_random, strict=False) - if x not in self.opt.Xi and x not in asked_non_tried + if x not in asked_non_tried ] asked_non_tried += [ - x for x in asked if x not in self.opt.Xi and x not in asked_non_tried + x for x in asked if x not in asked_non_tried ] i += 1 @@ -212,7 +219,7 @@ class Hyperopt: is_random_non_tried[: min(len(asked_non_tried), n_points)], ) else: - return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] + return self.get_optuna_asked_points(n_points=n_points, dimensions=dimensions), [False for _ in range(n_points)] def evaluate_result(self, val: dict[str, Any], current: int, is_random: bool): """ @@ -258,9 +265,7 @@ class Hyperopt: config_jobs = self.config.get("hyperopt_jobs", -1) logger.info(f"Number of parallel jobs set as: {config_jobs}") - self.opt = self.hyperopter.get_optimizer( - config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE - ) + self.opt = self.hyperopter.get_optimizer(self.random_state) self._setup_logging_mp_workaround() try: with Parallel(n_jobs=config_jobs) as parallel: @@ -276,9 +281,9 @@ class Hyperopt: if self.analyze_per_epoch: # First analysis not in parallel mode when using --analyze-per-epoch. # This allows dataprovider to load it's informative cache. - asked, is_random = self.get_asked_points(n_points=1) - f_val0 = self.hyperopter.generate_optimizer(asked[0]) - self.opt.tell(asked, [f_val0["loss"]]) + asked, is_random = self.get_asked_points(n_points=1, dimensions=self.hyperopter.o_dimensions) + f_val0 = self.hyperopter.generate_optimizer(asked[0].params) + self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) start += 1 @@ -290,9 +295,12 @@ class Hyperopt: n_rest = (i + 1) * jobs - (self.total_epochs - start) current_jobs = jobs - n_rest if n_rest > 0 else jobs - asked, is_random = self.get_asked_points(n_points=current_jobs) - f_val = self.run_optimizer_parallel(parallel, asked) - self.opt.tell(asked, [v["loss"] for v in f_val]) + asked, is_random = self.get_asked_points( + n_points=current_jobs, dimensions=self.hyperopter.o_dimensions) + f_val = self.run_optimizer_parallel(parallel, [asked1.params for asked1 in asked]) + for o_ask, v in zip(asked, f_val): + self.opt.tell(o_ask, v["loss"]) + # self.opt.tell(asked, [v["loss"] for v in f_val]) for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 6382d7f8d..d9a43e580 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -44,11 +44,12 @@ class IHyperOpt(ABC): def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType: """ Return base_estimator. - Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class - inheriting from RegressorMixin (from sklearn). + Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler" + "NSGAIIISampler", "QMCSampler" or an instance of a class + inheriting from BaseSampler (from optuna.samplers). """ - return "ET" - + return "NSGAIISampler" + def generate_roi_table(self, params: dict) -> dict[int, float]: """ Create a ROI table. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 9e4995aca..73f78352e 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -30,11 +30,14 @@ from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.util.dry_run_wallet import get_dry_run_wallet - # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) - from skopt import Optimizer + # from skopt import Optimizer + from freqtrade.optimize.space.decimalspace import SKDecimal + from skopt.space import Categorical, Integer, Real + import optuna + import optunahub from skopt.space import Dimension logger = logging.getLogger(__name__) @@ -58,6 +61,7 @@ class HyperOptimizer: self.trailing_space: list[Dimension] = [] self.max_open_trades_space: list[Dimension] = [] self.dimensions: list[Dimension] = [] + self.o_dimensions: Dict = {} self.config = config self.min_date: datetime @@ -127,8 +131,9 @@ class HyperOptimizer: self.hyperopt_pickle_magic(modules.__bases__) def _get_params_dict( - self, dimensions: list[Dimension], raw_params: list[Any] + self, dimensions: list[Dimension], raw_params: dict[str, Any] # list[Any] ) -> dict[str, Any]: + # logger.info(f"_get_params_dict: {raw_params}") # Ensure the number of dimensions match # the number of parameters in the list. if len(raw_params) != len(dimensions): @@ -136,7 +141,10 @@ class HyperOptimizer: # Return a dict where the keys are the names of the dimensions # and the values are taken from the list of parameters. - return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} + # result = {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} + # logger.info(f"d_get_params_dict: {result}") + # return {d.name: v for d, v in zip(dimensions, raw_params.params, strict=False)} + return raw_params def _get_params_details(self, params: dict) -> dict: """ @@ -377,33 +385,78 @@ class HyperOptimizer: "total_profit": total_profit, } + + def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict: + o_dimensions = {} + for original_dim in s_dimensions: + if type(original_dim) == Integer: # isinstance(original_dim, Integer): + o_dimensions[original_dim.name] = optuna.distributions.IntDistribution( + original_dim.low, original_dim.high, log=False, step=1 + ) + elif ( + type(original_dim) == SKDecimal + ): + o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( + original_dim.low_orig, + original_dim.high_orig, + log=False, + step=1 / pow(10, original_dim.decimals) + ) + elif ( + type(original_dim) == Real + ): + o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( + original_dim.low, + original_dim.high, + log=False, + ) + elif ( + type(original_dim) == Categorical + ): + o_dimensions[original_dim.name] = optuna.distributions.CategoricalDistribution( + list(original_dim.bounds) + ) + else: + raise Exception( + f"Unknown search space {original_dim} / {type(original_dim)}" + ) + # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") + return o_dimensions + def get_optimizer( self, - cpu_count: int, random_state: int, - initial_points: int, - model_queue_size: int, - ) -> Optimizer: - dimensions = self.dimensions - estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions) + ): - acq_optimizer = "sampling" - if isinstance(estimator, str): - if estimator not in ("GP", "RF", "ET", "GBRT"): - raise OperationalException(f"Estimator {estimator} not supported.") - else: - acq_optimizer = "auto" + o_sampler = self.custom_hyperopt.generate_estimator(dimensions=self.dimensions) + self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) - logger.info(f"Using estimator {estimator}.") - return Optimizer( - dimensions, - base_estimator=estimator, - acq_optimizer=acq_optimizer, - n_initial_points=initial_points, - acq_optimizer_kwargs={"n_jobs": cpu_count}, - random_state=random_state, - model_queue_size=model_queue_size, - ) + # for save/restore + # with open("sampler.pkl", "wb") as fout: + # pickle.dump(study.sampler, fout) + # restored_sampler = pickle.load(open("sampler.pkl", "rb")) + + if isinstance(o_sampler, str): + if o_sampler not in ("TPESampler", "GPSampler", "CmaEsSampler", + "NSGAIISampler", "NSGAIIISampler", "QMCSampler" + ): + raise OperationalException(f"Optuna Sampler {o_sampler} not supported.") + + if o_sampler == "TPESampler": + sampler = optuna.samplers.TPESampler(seed=random_state) + elif o_sampler == "GPSampler": + sampler = optuna.samplers.GPSampler(seed=random_state) + elif o_sampler == "CmaEsSampler": + sampler = optuna.samplers.CmaEsSampler(seed=random_state) + elif o_sampler == "NSGAIISampler": + sampler = optuna.samplers.NSGAIISampler(seed=random_state) + elif o_sampler == "NSGAIIISampler": + sampler = optuna.samplers.NSGAIIISampler(seed=random_state) + elif o_sampler == "QMCSampler": + sampler = optuna.samplers.QMCSampler(seed=random_state) + + logger.info(f"Using optuna sampler {o_sampler}.") + return optuna.create_study(sampler=sampler, direction="minimize") def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]: preprocessed = self.backtesting.strategy.advise_all_indicators(data) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2a326d654..fdec43e02 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,3 +6,6 @@ scipy==1.15.2 scikit-learn==1.6.1 ft-scikit-optimize==0.9.2 filelock==3.18.0 +optuna==4.2.1 +optunahub==0.2.0 +cmaes==0.11.1 \ No newline at end of file diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a0cc4eb73..450bb618c 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -606,9 +606,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( - list(optimizer_param.values()) - ) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) + # list(optimizer_param.values()) + assert generate_optimizer_value == response_expected @@ -1088,8 +1088,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None assert opt.backtesting.strategy.max_open_trades != 1 opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1" - with pytest.raises(OperationalException, match="Estimator ET1 not supported."): - opt.get_optimizer(2, 42, 2, 2) + with pytest.raises(OperationalException, match="Optuna Sampler ET1 not supported."): + opt.get_optimizer(42) @pytest.mark.filterwarnings("ignore::DeprecationWarning") @@ -1313,9 +1313,10 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No @wraps(func) def wrapper(*args, **kwargs): nonlocal first_time_evaluated + print(f"max_open_trades: {hyperopt.hyperopter.backtesting.strategy.max_open_trades}") stake_amount = func(*args, **kwargs) if first_time_evaluated is False: - assert stake_amount == 1 + assert stake_amount == 2 first_time_evaluated = True return stake_amount @@ -1329,5 +1330,5 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No hyperopt.start() - assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 8 - assert hyperopt.config["max_open_trades"] == 8 + assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 4 + assert hyperopt.config["max_open_trades"] == 4 From c5088e6b66c8b3b5c16de8d965b2bb077dc2afdb Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Tue, 25 Mar 2025 15:07:09 +0200 Subject: [PATCH 02/64] fix formatting --- freqtrade/optimize/hyperopt/hyperopt.py | 36 ++++++++------ .../optimize/hyperopt/hyperopt_interface.py | 2 +- .../optimize/hyperopt/hyperopt_optimizer.py | 49 +++++++++---------- tests/optimize/test_hyperopt.py | 4 +- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 9eb5c9531..e48e0eba0 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -171,7 +171,9 @@ class Hyperopt: asked.append(self.opt.ask(dimensions)) return asked - def get_asked_points(self, n_points: int, dimensions: dict) -> tuple[list[list[Any]], list[bool]]: + def get_asked_points( + self, n_points: int, dimensions: dict + ) -> tuple[list[list[Any]], list[bool]]: """ Enforce points returned from `self.opt.ask` have not been already evaluated @@ -197,20 +199,19 @@ class Hyperopt: while i < 5 and len(asked_non_tried) < n_points: if i < 3: self.opt.cache_ = {} - asked = unique_list(self.get_optuna_asked_points(n_points=n_points * 5 if i > 0 else n_points, - dimensions=dimensions)) + asked = unique_list( + self.get_optuna_asked_points( + n_points=n_points * 5 if i > 0 else n_points, dimensions=dimensions + ) + ) is_random = [False for _ in range(len(asked))] else: asked = unique_list(self.opt.space.rvs(n_samples=n_points * 5)) is_random = [True for _ in range(len(asked))] is_random_non_tried += [ - rand - for x, rand in zip(asked, is_random, strict=False) - if x not in asked_non_tried - ] - asked_non_tried += [ - x for x in asked if x not in asked_non_tried + rand for x, rand in zip(asked, is_random, strict=False) if x not in asked_non_tried ] + asked_non_tried += [x for x in asked if x not in asked_non_tried] i += 1 if asked_non_tried: @@ -219,7 +220,9 @@ class Hyperopt: is_random_non_tried[: min(len(asked_non_tried), n_points)], ) else: - return self.get_optuna_asked_points(n_points=n_points, dimensions=dimensions), [False for _ in range(n_points)] + return self.get_optuna_asked_points(n_points=n_points, dimensions=dimensions), [ + False for _ in range(n_points) + ] def evaluate_result(self, val: dict[str, Any], current: int, is_random: bool): """ @@ -281,7 +284,9 @@ class Hyperopt: if self.analyze_per_epoch: # First analysis not in parallel mode when using --analyze-per-epoch. # This allows dataprovider to load it's informative cache. - asked, is_random = self.get_asked_points(n_points=1, dimensions=self.hyperopter.o_dimensions) + asked, is_random = self.get_asked_points( + n_points=1, dimensions=self.hyperopter.o_dimensions + ) f_val0 = self.hyperopter.generate_optimizer(asked[0].params) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) @@ -296,9 +301,12 @@ class Hyperopt: current_jobs = jobs - n_rest if n_rest > 0 else jobs asked, is_random = self.get_asked_points( - n_points=current_jobs, dimensions=self.hyperopter.o_dimensions) - f_val = self.run_optimizer_parallel(parallel, [asked1.params for asked1 in asked]) - for o_ask, v in zip(asked, f_val): + n_points=current_jobs, dimensions=self.hyperopter.o_dimensions + ) + f_val = self.run_optimizer_parallel( + parallel, [asked1.params for asked1 in asked] + ) + for o_ask, v in zip(asked, f_val, strict=False): self.opt.tell(o_ask, v["loss"]) # self.opt.tell(asked, [v["loss"] for v in f_val]) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index d9a43e580..34ee09e90 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -49,7 +49,7 @@ class IHyperOpt(ABC): inheriting from BaseSampler (from optuna.samplers). """ return "NSGAIISampler" - + def generate_roi_table(self, params: dict) -> dict[int, float]: """ Create a ROI table. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 73f78352e..87e2feec7 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -30,15 +30,15 @@ from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.util.dry_run_wallet import get_dry_run_wallet + # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) # from skopt import Optimizer - from freqtrade.optimize.space.decimalspace import SKDecimal - from skopt.space import Categorical, Integer, Real import optuna - import optunahub - from skopt.space import Dimension + from skopt.space import Categorical, Dimension, Integer, Real + + from freqtrade.optimize.space.decimalspace import SKDecimal logger = logging.getLogger(__name__) @@ -61,7 +61,7 @@ class HyperOptimizer: self.trailing_space: list[Dimension] = [] self.max_open_trades_space: list[Dimension] = [] self.dimensions: list[Dimension] = [] - self.o_dimensions: Dict = {} + self.o_dimensions: dict = {} self.config = config self.min_date: datetime @@ -131,7 +131,9 @@ class HyperOptimizer: self.hyperopt_pickle_magic(modules.__bases__) def _get_params_dict( - self, dimensions: list[Dimension], raw_params: dict[str, Any] # list[Any] + self, + dimensions: list[Dimension], + raw_params: dict[str, Any], ) -> dict[str, Any]: # logger.info(f"_get_params_dict: {raw_params}") # Ensure the number of dimensions match @@ -253,7 +255,7 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]: + def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: # list[Any] """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -385,41 +387,32 @@ class HyperOptimizer: "total_profit": total_profit, } - def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict: o_dimensions = {} for original_dim in s_dimensions: - if type(original_dim) == Integer: # isinstance(original_dim, Integer): + if isinstance(original_dim, Integer): o_dimensions[original_dim.name] = optuna.distributions.IntDistribution( original_dim.low, original_dim.high, log=False, step=1 ) - elif ( - type(original_dim) == SKDecimal - ): + elif isinstance(original_dim, SKDecimal): o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( original_dim.low_orig, original_dim.high_orig, log=False, - step=1 / pow(10, original_dim.decimals) + step=1 / pow(10, original_dim.decimals), ) - elif ( - type(original_dim) == Real - ): + elif isinstance(original_dim, Real): o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( original_dim.low, original_dim.high, log=False, ) - elif ( - type(original_dim) == Categorical - ): + elif isinstance(original_dim, Categorical): o_dimensions[original_dim.name] = optuna.distributions.CategoricalDistribution( list(original_dim.bounds) ) else: - raise Exception( - f"Unknown search space {original_dim} / {type(original_dim)}" - ) + raise Exception(f"Unknown search space {original_dim} / {type(original_dim)}") # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") return o_dimensions @@ -427,7 +420,6 @@ class HyperOptimizer: self, random_state: int, ): - o_sampler = self.custom_hyperopt.generate_estimator(dimensions=self.dimensions) self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) @@ -437,9 +429,14 @@ class HyperOptimizer: # restored_sampler = pickle.load(open("sampler.pkl", "rb")) if isinstance(o_sampler, str): - if o_sampler not in ("TPESampler", "GPSampler", "CmaEsSampler", - "NSGAIISampler", "NSGAIIISampler", "QMCSampler" - ): + if o_sampler not in ( + "TPESampler", + "GPSampler", + "CmaEsSampler", + "NSGAIISampler", + "NSGAIIISampler", + "QMCSampler", + ): raise OperationalException(f"Optuna Sampler {o_sampler} not supported.") if o_sampler == "TPESampler": diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 450bb618c..3ca35ca04 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -607,8 +607,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) - # list(optimizer_param.values()) - + # list(optimizer_param.values()) + assert generate_optimizer_value == response_expected From fcd0c1d606c7c4799e7cbc8900e007978fd6c5e4 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Tue, 25 Mar 2025 19:16:08 +0200 Subject: [PATCH 03/64] formatting fix --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 87e2feec7..14eedf55f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -255,7 +255,7 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: # list[Any] + def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: # list[Any] """ Used Optimize function. Called once per epoch to optimize whatever is configured. From eb03382b2d6434aaa603cb6835052c04cec359d6 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 08:36:55 +0200 Subject: [PATCH 04/64] remove SKOPT_MODEL_QUEUE_SIZE comments --- freqtrade/optimize/hyperopt/hyperopt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index e48e0eba0..f31f875bb 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -35,9 +35,6 @@ logger = logging.getLogger(__name__) INITIAL_POINTS = 30 -# Keep no more than SKOPT_MODEL_QUEUE_SIZE models -# in the skopt model queue, to optimize memory consumption -# SKOPT_MODEL_QUEUE_SIZE = 10 log_queue: Any From 31c4d35932d8b3bb43eb41dc307c27b3000318fb Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 08:44:46 +0200 Subject: [PATCH 05/64] update docs for optuna sampler --- docs/advanced-hyperopt.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index b12d8f5b6..a8458f90b 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -161,30 +161,20 @@ class MyAwesomeStrategy(IStrategy): ### Overriding Base estimator -You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. +You can define your own optuna sampler for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. ```python class MyAwesomeStrategy(IStrategy): class HyperOpt: def generate_estimator(dimensions: List['Dimension'], **kwargs): - return "RF" + return "TPESampler" ``` -Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`". +Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIIISampler", "QMCSampler" (Details can be found in the [optuna-samplers documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html)), or "an instance of a class that inherits from `optuna.samplers.BaseSampler`". Some research will be necessary to find additional Regressors. -Example for `ExtraTreesRegressor` ("ET") with additional parameters: - -```python -class MyAwesomeStrategy(IStrategy): - class HyperOpt: - def generate_estimator(dimensions: List['Dimension'], **kwargs): - from skopt.learning import ExtraTreesRegressor - # Corresponds to "ET" - but allows additional parameters. - return ExtraTreesRegressor(n_estimators=100) - ``` The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example: From 2e06eb0e7b51a037a9231f3940763908574aa4f1 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 08:46:05 +0200 Subject: [PATCH 06/64] update docs for optuna sampler --- docs/advanced-hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index a8458f90b..e12d700f3 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -173,7 +173,7 @@ class MyAwesomeStrategy(IStrategy): Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIIISampler", "QMCSampler" (Details can be found in the [optuna-samplers documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html)), or "an instance of a class that inherits from `optuna.samplers.BaseSampler`". -Some research will be necessary to find additional Regressors. +Some research will be necessary to find additional Samplers (from optunahub) for example. ``` From 553dbccec765045433b843a920818f1f17cbe9e3 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 16:42:09 +0200 Subject: [PATCH 07/64] simplify get_optimizer --- freqtrade/optimize/hyperopt/hyperopt.py | 4 +- .../optimize/hyperopt/hyperopt_optimizer.py | 45 ++++++++----------- requirements-hyperopt.txt | 1 - 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index f31f875bb..f8e609c87 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -170,7 +170,7 @@ class Hyperopt: def get_asked_points( self, n_points: int, dimensions: dict - ) -> tuple[list[list[Any]], list[bool]]: + ) -> tuple[list[Any], list[bool]]: """ Enforce points returned from `self.opt.ask` have not been already evaluated @@ -300,6 +300,8 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=current_jobs, dimensions=self.hyperopter.o_dimensions ) + # asked_params = [asked1.params for asked1 in asked] + # logger.info(f"asked iteration {i}: {asked_params}") f_val = self.run_optimizer_parallel( parallel, [asked1.params for asked1 in asked] ) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 14eedf55f..fc0dde29f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -45,6 +45,14 @@ logger = logging.getLogger(__name__) MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization +optuna_samplers_dict = { + "TPESampler": optuna.samplers.TPESampler, + "GPSampler": optuna.samplers.GPSampler, + "CmaEsSampler": optuna.samplers.CmaEsSampler, + "NSGAIISampler": optuna.samplers.NSGAIISampler, + "NSGAIIISampler": optuna.samplers.NSGAIIISampler, + "QMCSampler": optuna.samplers.QMCSampler +} class HyperOptimizer: """ @@ -390,17 +398,17 @@ class HyperOptimizer: def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict: o_dimensions = {} for original_dim in s_dimensions: - if isinstance(original_dim, Integer): - o_dimensions[original_dim.name] = optuna.distributions.IntDistribution( - original_dim.low, original_dim.high, log=False, step=1 - ) - elif isinstance(original_dim, SKDecimal): + if isinstance(original_dim, SKDecimal): o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( original_dim.low_orig, original_dim.high_orig, log=False, step=1 / pow(10, original_dim.decimals), ) + elif isinstance(original_dim, Integer): + o_dimensions[original_dim.name] = optuna.distributions.IntDistribution( + original_dim.low, original_dim.high, log=False, step=1 + ) elif isinstance(original_dim, Real): o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( original_dim.low, @@ -413,7 +421,7 @@ class HyperOptimizer: ) else: raise Exception(f"Unknown search space {original_dim} / {type(original_dim)}") - # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") + # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") return o_dimensions def get_optimizer( @@ -429,28 +437,11 @@ class HyperOptimizer: # restored_sampler = pickle.load(open("sampler.pkl", "rb")) if isinstance(o_sampler, str): - if o_sampler not in ( - "TPESampler", - "GPSampler", - "CmaEsSampler", - "NSGAIISampler", - "NSGAIIISampler", - "QMCSampler", - ): + if o_sampler not in optuna_samplers_dict.keys(): raise OperationalException(f"Optuna Sampler {o_sampler} not supported.") - - if o_sampler == "TPESampler": - sampler = optuna.samplers.TPESampler(seed=random_state) - elif o_sampler == "GPSampler": - sampler = optuna.samplers.GPSampler(seed=random_state) - elif o_sampler == "CmaEsSampler": - sampler = optuna.samplers.CmaEsSampler(seed=random_state) - elif o_sampler == "NSGAIISampler": - sampler = optuna.samplers.NSGAIISampler(seed=random_state) - elif o_sampler == "NSGAIIISampler": - sampler = optuna.samplers.NSGAIIISampler(seed=random_state) - elif o_sampler == "QMCSampler": - sampler = optuna.samplers.QMCSampler(seed=random_state) + sampler = optuna_samplers_dict[o_sampler](seed=random_state) + else: + sampler = o_sampler logger.info(f"Using optuna sampler {o_sampler}.") return optuna.create_study(sampler=sampler, direction="minimize") diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index fdec43e02..09cfabe4c 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,5 +7,4 @@ scikit-learn==1.6.1 ft-scikit-optimize==0.9.2 filelock==3.18.0 optuna==4.2.1 -optunahub==0.2.0 cmaes==0.11.1 \ No newline at end of file From 59e52bb6017b1af16f5294cbfe392122c3172796 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 16:55:43 +0200 Subject: [PATCH 08/64] fix type-errors by declaring the type of o_dimensions in convert_dimensions_to_optuna_space --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index fc0dde29f..023107224 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -54,6 +54,7 @@ optuna_samplers_dict = { "QMCSampler": optuna.samplers.QMCSampler } + class HyperOptimizer: """ HyperoptOptimizer class @@ -396,7 +397,7 @@ class HyperOptimizer: } def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict: - o_dimensions = {} + o_dimensions: dict[str, optuna.distributions.BaseDistribution] = {} for original_dim in s_dimensions: if isinstance(original_dim, SKDecimal): o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( From ee3d46c8faa3d17ac0b07db06d2a76f34362d8e7 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 21:25:56 +0200 Subject: [PATCH 09/64] change default optimizer to NSGAIIISampler - best results so far --- freqtrade/optimize/hyperopt/hyperopt_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 34ee09e90..aae1d22e7 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -48,7 +48,7 @@ class IHyperOpt(ABC): "NSGAIIISampler", "QMCSampler" or an instance of a class inheriting from BaseSampler (from optuna.samplers). """ - return "NSGAIISampler" + return "NSGAIIISampler" def generate_roi_table(self, params: dict) -> dict[int, float]: """ From 6b78b1c882a30f3870295aba1c383bb6b1129c15 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 26 Mar 2025 21:30:18 +0200 Subject: [PATCH 10/64] fix formatting --- freqtrade/optimize/hyperopt/hyperopt.py | 4 +--- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index f8e609c87..4dc720475 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -168,9 +168,7 @@ class Hyperopt: asked.append(self.opt.ask(dimensions)) return asked - def get_asked_points( - self, n_points: int, dimensions: dict - ) -> tuple[list[Any], list[bool]]: + def get_asked_points(self, n_points: int, dimensions: dict) -> tuple[list[Any], list[bool]]: """ Enforce points returned from `self.opt.ask` have not been already evaluated diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 023107224..8e1daade8 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -51,7 +51,7 @@ optuna_samplers_dict = { "CmaEsSampler": optuna.samplers.CmaEsSampler, "NSGAIISampler": optuna.samplers.NSGAIISampler, "NSGAIIISampler": optuna.samplers.NSGAIIISampler, - "QMCSampler": optuna.samplers.QMCSampler + "QMCSampler": optuna.samplers.QMCSampler, } From 9c1183bc598551d40516457cdabded2c9aa1c40b Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Fri, 28 Mar 2025 08:12:37 +0200 Subject: [PATCH 11/64] fix formatting requirements-hyperopt.txt --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 09cfabe4c..cb98d6bb7 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==1.6.1 ft-scikit-optimize==0.9.2 filelock==3.18.0 optuna==4.2.1 -cmaes==0.11.1 \ No newline at end of file +cmaes==0.11.1 From 2595479e4348c56ffae8d0ee65d1a58ae3264bee Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 30 Mar 2025 21:11:26 +0300 Subject: [PATCH 12/64] 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 From 85f4a8daea386c1a6ecf490dc29c598061b552dd Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Mon, 31 Mar 2025 00:10:52 +0300 Subject: [PATCH 13/64] fix formatting --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 6 +++--- freqtrade/strategy/parameters.py | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index d60a272ea..944da5258 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -422,9 +422,9 @@ class HyperOptimizer: list(original_dim.bounds) ) # for preparing to remove old skopt spaces - elif isinstance( - original_dim, ft_CategoricalDistribution - ) or isinstance(original_dim, ft_IntDistribution): + 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)}") diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 17f6da006..24047c844 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -27,6 +27,7 @@ logger = logging.getLogger(__name__) class ft_CategoricalDistribution(CategoricalDistribution): name: str + def __init__( self, categories: Sequence[Any], @@ -34,8 +35,10 @@ class ft_CategoricalDistribution(CategoricalDistribution): ): return super().__init__(categories) + class ft_IntDistribution(IntDistribution): name: str + def __init__( self, low: int, @@ -44,6 +47,7 @@ class ft_IntDistribution(IntDistribution): ): return super().__init__(low, high, **kwargs) + class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. @@ -88,9 +92,9 @@ class BaseParameter(ABC): return f"{self.__class__.__name__}({self.value})" @abstractmethod - def get_space(self, name: str) -> Union[ - "ft_IntDistribution", "Real", "SKDecimal", "ft_CategoricalDistribution" - ]: + 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 """ @@ -187,9 +191,7 @@ class IntParameter(NumericParameter): :param name: A name of parameter field. """ # return Integer(low=self.low, high=self.high, name=name, **self._space_params) - result = ft_IntDistribution( - self.low, self.high, **self._space_params - ) + result = ft_IntDistribution(self.low, self.high, **self._space_params) result.name = name return result From 3fcf6559abc646d5da7ab04b0cdbf8c55c2fd31d Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Mon, 31 Mar 2025 13:48:12 +0300 Subject: [PATCH 14/64] change from skopt.space.Real to optuna.distributions.FloatDistribution --- .../optimize/hyperopt/hyperopt_interface.py | 28 +++++++--- .../optimize/hyperopt/hyperopt_optimizer.py | 33 +++++------ freqtrade/strategy/parameters.py | 55 +++++++++++-------- tests/optimize/test_hyperopt.py | 7 ++- tests/strategy/test_interface.py | 5 +- 5 files changed, 73 insertions(+), 55 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index aae1d22e7..d88a0a27a 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -9,13 +9,17 @@ from abc import ABC from typing import TypeAlias from sklearn.base import RegressorMixin -from skopt.space import Categorical, Dimension, Integer +from skopt.space import Dimension # , Integer, Categorical, from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy +from freqtrade.strategy.parameters import ( + ft_CategoricalDistribution, + ft_IntDistribution, +) logger = logging.getLogger(__name__) @@ -133,9 +137,12 @@ class IHyperOpt(ABC): logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ - Integer(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], name="roi_t1"), - Integer(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], name="roi_t2"), - Integer(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], name="roi_t3"), + # Integer(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], name="roi_t1"), + ft_IntDistribution("roi_t1", roi_limits["roi_t1_min"], roi_limits["roi_t1_max"]), + # Integer(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], name="roi_t2"), + ft_IntDistribution("roi_t2", roi_limits["roi_t2_min"], roi_limits["roi_t2_max"]), + # Integer(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], name="roi_t3"), + ft_IntDistribution("roi_t3", roi_limits["roi_t3_min"], roi_limits["roi_t3_max"]), SKDecimal( roi_limits["roi_p1_min"], roi_limits["roi_p1_max"], decimals=3, name="roi_p1" ), @@ -184,7 +191,8 @@ class IHyperOpt(ABC): # This parameter is included into the hyperspace dimensions rather than assigning # it explicitly in the code in order to have it printed in the results along with # other 'trailing' hyperspace parameters. - Categorical([True], name="trailing_stop"), + # Categorical([True], name="trailing_stop"), + ft_CategoricalDistribution("trailing_stop", [True]), SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between @@ -192,7 +200,8 @@ class IHyperOpt(ABC): # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"), - Categorical([True, False], name="trailing_only_offset_is_reached"), + # Categorical([True, False], name="trailing_only_offset_is_reached"), + ft_CategoricalDistribution("trailing_only_offset_is_reached", [True, False]), ] def max_open_trades_space(self) -> list[Dimension]: @@ -201,9 +210,10 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ - return [ - Integer(-1, 10, name="max_open_trades"), - ] + # return [ + # Integer(-1, 10, name="max_open_trades"), + # ] + return [ft_IntDistribution("max_open_trades", -1, 10)] # This is needed for proper unpickling the class attribute timeframe # which is set to the actual value by the resolver. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 944da5258..af42f486d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -36,10 +36,14 @@ with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) # from skopt import Optimizer import optuna - from skopt.space import Categorical, Dimension, Integer, Real + from skopt.space import Dimension from freqtrade.optimize.space.decimalspace import SKDecimal - from freqtrade.strategy.parameters import ft_CategoricalDistribution, ft_IntDistribution + from freqtrade.strategy.parameters import ( + ft_CategoricalDistribution, + ft_FloatDistribution, + ft_IntDistribution, + ) logger = logging.getLogger(__name__) @@ -407,27 +411,18 @@ class HyperOptimizer: log=False, step=1 / pow(10, original_dim.decimals), ) - elif isinstance(original_dim, Integer): - o_dimensions[original_dim.name] = optuna.distributions.IntDistribution( - original_dim.low, original_dim.high, log=False, step=1 - ) - elif isinstance(original_dim, Real): - o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( - original_dim.low, - original_dim.high, - log=False, - ) - elif isinstance(original_dim, Categorical): - 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 + elif ( + isinstance(original_dim, ft_CategoricalDistribution) + or isinstance(original_dim, ft_IntDistribution) + or isinstance(original_dim, ft_FloatDistribution) ): o_dimensions[original_dim.name] = original_dim else: - raise Exception(f"Unknown search space {original_dim} / {type(original_dim)}") + raise Exception( + f"Unknown search space {original_dim.name} - {original_dim} / \ + {type(original_dim)}" + ) # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") return o_dimensions diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 24047c844..f1b025644 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -14,9 +14,9 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): - from optuna.distributions import CategoricalDistribution, IntDistribution - from skopt.space import Integer, Real # Categorical + from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution + # from skopt.space import Integer, Real # Categorical from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException @@ -26,25 +26,37 @@ logger = logging.getLogger(__name__) class ft_CategoricalDistribution(CategoricalDistribution): - name: str - def __init__( self, + name: str, categories: Sequence[Any], **kwargs, ): + self.name = name return super().__init__(categories) class ft_IntDistribution(IntDistribution): - name: str - def __init__( self, - low: int, - high: int, + name: str, + low: int | float, + high: int | float, **kwargs, ): + self.name = name + return super().__init__(int(low), int(high), **kwargs) + + +class ft_FloatDistribution(FloatDistribution): + def __init__( + self, + name: str, + low: float, + high: float, + **kwargs, + ): + self.name = name return super().__init__(low, high, **kwargs) @@ -76,7 +88,7 @@ class BaseParameter(ABC): :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. :param kwargs: Extra parameters to optuna.distributions. - (IntDistribution|Real|CategoricalDistribution). + (IntDistribution|FloatDistribution|CategoricalDistribution). """ if "name" in kwargs: raise OperationalException( @@ -94,7 +106,9 @@ class BaseParameter(ABC): @abstractmethod def get_space( self, name: str - ) -> Union["ft_IntDistribution", "Real", "SKDecimal", "ft_CategoricalDistribution"]: + ) -> Union[ + "ft_IntDistribution", "ft_FloatDistribution", "SKDecimal", "ft_CategoricalDistribution" + ]: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ @@ -136,7 +150,7 @@ class NumericParameter(BaseParameter): 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.*. + :param kwargs: Extra parameters to optuna.distributions.*. """ if high is not None and isinstance(low, Sequence): raise OperationalException(f"{self.__class__.__name__} space invalid.") @@ -185,15 +199,13 @@ class IntParameter(NumericParameter): 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) -> "ft_IntDistribution": """ Create optuna distribution space. :param name: A name of parameter field. """ # 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 + return ft_IntDistribution(name, self.low, self.high, **self._space_params) @property def range(self): @@ -235,18 +247,19 @@ class RealParameter(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.Real. + :param kwargs: Extra parameters to optuna.distributions.FloatDistribution. """ super().__init__( low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs ) - def get_space(self, name: str) -> "Real": + def get_space(self, name: str) -> "ft_FloatDistribution": """ Create skopt optimization space. :param name: A name of parameter field. """ - return Real(low=self.low, high=self.high, name=name, **self._space_params) + return ft_FloatDistribution(name, self.low, self.high, **self._space_params) + # return Real(low=self.low, high=self.high, name=name, **self._space_params) class DecimalParameter(NumericParameter): @@ -349,10 +362,8 @@ class CategoricalParameter(BaseParameter): Create optuna distribution space. :param name: A name of parameter field. """ - # Categorical(self.opt_range, name=name, **self._space_params) - result = ft_CategoricalDistribution(self.opt_range) - result.name = name - return result + # return Categorical(self.opt_range, name=name, **self._space_params) + return ft_CategoricalDistribution(name, self.opt_range) @property def range(self): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 3ca35ca04..ad684dd19 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -7,7 +7,6 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest from filelock import Timeout -from skopt.space import Integer from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data @@ -19,6 +18,9 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter + +# from skopt.space import Integer +from freqtrade.strategy.parameters import ft_IntDistribution from freqtrade.util import dt_utc from tests.conftest import ( CURRENT_TEST_STRATEGY, @@ -1304,7 +1306,8 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) hyperopt.hyperopter.custom_hyperopt.max_open_trades_space = lambda: [ - Integer(1, 10, name="max_open_trades") + # Integer(1, 10, name="max_open_trades") + ft_IntDistribution("max_open_trades", 1, 10) ] first_time_evaluated = False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 8cbe40d13..b512e578c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -895,8 +895,7 @@ def test_is_informative_pairs_callback(default_conf): def test_hyperopt_parameters(): HyperoptStateContainer.set_state(HyperoptState.INDICATORS) - from optuna.distributions import CategoricalDistribution, IntDistribution - from skopt.space import Real + from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution with pytest.raises(OperationalException, match=r"Name is determined.*"): IntParameter(low=0, high=5, default=1, name="hello") @@ -939,7 +938,7 @@ def test_hyperopt_parameters(): fltpar = RealParameter(low=0.0, high=5.5, default=1.0, space="buy") assert fltpar.value == 1 - assert isinstance(fltpar.get_space(""), Real) + assert isinstance(fltpar.get_space(""), FloatDistribution) fltpar = DecimalParameter(low=0.0, high=0.5, default=0.14, decimals=1, space="buy") assert fltpar.value == 0.1 From 90aaaa50fcce5808935299f8f89d7f3bb83f56a0 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 2 Apr 2025 18:45:49 +0300 Subject: [PATCH 15/64] fix increasing memory usage. --- freqtrade/optimize/hyperopt/hyperopt.py | 33 ++++++--- .../optimize/hyperopt/hyperopt_optimizer.py | 69 ++++++++++--------- tests/optimize/test_hyperopt.py | 4 +- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 4dc720475..5c906cb9a 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -4,6 +4,7 @@ This module contains the hyperopt logic """ +import gc import logging import random from datetime import datetime @@ -19,6 +20,7 @@ from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural +from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.hyperopt.hyperopt_logger import logging_mp_handle, logging_mp_setup from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput @@ -30,6 +32,9 @@ from freqtrade.optimize.hyperopt_tools import ( from freqtrade.util import get_progress_tracker +# import multiprocessing as mp +# mp.set_start_method('fork', force=True) # spawn fork forkserver + logger = logging.getLogger(__name__) @@ -90,6 +95,7 @@ class Hyperopt: self.print_json = self.config.get("print_json", False) self.hyperopter = HyperOptimizer(self.config) + self.hyperopter.data_pickle_file = self.data_pickle_file @staticmethod def get_lock_filename(config: Config) -> str: @@ -146,7 +152,9 @@ class Hyperopt: self.print_all, ) - def run_optimizer_parallel(self, parallel: Parallel, asked: list[list]) -> list[dict[str, Any]]: + def run_optimizer_parallel( + self, parallel: Parallel, backtesting: Backtesting, asked: list[list] + ) -> list[dict[str, Any]]: """Start optimizer in a parallel way""" def optimizer_wrapper(*args, **kwargs): @@ -157,7 +165,9 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) - return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) + return parallel( + delayed(wrap_non_picklable_objects(optimizer_wrapper))(backtesting, v) for v in asked + ) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 @@ -282,7 +292,9 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=1, dimensions=self.hyperopter.o_dimensions ) - f_val0 = self.hyperopter.generate_optimizer(asked[0].params) + f_val0 = self.hyperopter.generate_optimizer( + self.hyperopter.backtesting, asked[0].params + ) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) @@ -299,13 +311,17 @@ class Hyperopt: n_points=current_jobs, dimensions=self.hyperopter.o_dimensions ) # asked_params = [asked1.params for asked1 in asked] - # logger.info(f"asked iteration {i}: {asked_params}") + # logger.info(f"asked iteration {i}: {asked} {asked_params}") + f_val = self.run_optimizer_parallel( - parallel, [asked1.params for asked1 in asked] + parallel, + self.hyperopter.backtesting, + [asked1.params for asked1 in asked], ) - for o_ask, v in zip(asked, f_val, strict=False): - self.opt.tell(o_ask, v["loss"]) - # self.opt.tell(asked, [v["loss"] for v in f_val]) + f_val_loss = [v["loss"] for v in f_val] + for o_ask, v in zip(asked, f_val_loss, strict=False): + self.opt.tell(o_ask, v) + # logger.info(f"result iteration {i}: {asked} {f_val_loss}") for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) @@ -314,6 +330,7 @@ class Hyperopt: self.evaluate_result(val, current, is_random[j]) pbar.update(task, advance=1) logging_mp_handle(log_queue) + gc.collect() except KeyboardInterrupt: print("User interrupted..") diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index af42f486d..19a30015e 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -7,12 +7,14 @@ import logging import sys import warnings from datetime import datetime, timezone +from pathlib import Path from typing import Any from joblib import dump, load from joblib.externals import cloudpickle from pandas import DataFrame +# from memory_profiler import profile from freqtrade.constants import DATETIME_PRINT_FORMAT, Config from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange @@ -31,10 +33,12 @@ from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.util.dry_run_wallet import get_dry_run_wallet -# Suppress scikit-learn FutureWarnings from skopt +# Suppress optuna ExperimentalWarning from skopt with warnings.catch_warnings(): + from optuna.exceptions import ExperimentalWarning + warnings.filterwarnings("ignore", category=FutureWarning) - # from skopt import Optimizer + # warnings.filterwarnings("ignore", category=ExperimentalWarning) import optuna from skopt.space import Dimension @@ -102,12 +106,8 @@ class HyperOptimizer: self.config ) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - - self.data_pickle_file = ( - self.config["user_data_dir"] / "hyperopt_results" / "hyperopt_tickerdata.pkl" - ) - self.market_change = 0.0 + self.data_pickle_file = "" if HyperoptTools.has_space(self.config, "sell"): # Make sure use_exit_signal is enabled @@ -260,16 +260,23 @@ class HyperOptimizer: + self.max_open_trades_space ) - def assign_params(self, params_dict: dict[str, Any], category: str) -> None: + def assign_params( + self, backtesting: Backtesting, params_dict: dict[str, Any], category: str + ) -> None: """ Assign hyperoptable parameters """ - for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category): + for attr_name, attr in backtesting.strategy.enumerate_parameters(category): if attr.optimize: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: # list[Any] + # @profile + # fp=open('memory_profiler.log','w+') + # @profile(stream=fp) + def generate_optimizer( + self, backtesting: Backtesting, raw_params: dict[str, Any] + ) -> dict[str, Any]: # list[Any] """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -281,30 +288,26 @@ class HyperOptimizer: # Apply parameters if HyperoptTools.has_space(self.config, "buy"): - self.assign_params(params_dict, "buy") + self.assign_params(backtesting, params_dict, "buy") if HyperoptTools.has_space(self.config, "sell"): - self.assign_params(params_dict, "sell") + self.assign_params(backtesting, params_dict, "sell") if HyperoptTools.has_space(self.config, "protection"): - self.assign_params(params_dict, "protection") + self.assign_params(backtesting, params_dict, "protection") if HyperoptTools.has_space(self.config, "roi"): - self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( - params_dict - ) + backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params_dict) if HyperoptTools.has_space(self.config, "stoploss"): - self.backtesting.strategy.stoploss = params_dict["stoploss"] + backtesting.strategy.stoploss = params_dict["stoploss"] if HyperoptTools.has_space(self.config, "trailing"): d = self.custom_hyperopt.generate_trailing_params(params_dict) - self.backtesting.strategy.trailing_stop = d["trailing_stop"] - self.backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] - self.backtesting.strategy.trailing_stop_positive_offset = d[ - "trailing_stop_positive_offset" - ] - self.backtesting.strategy.trailing_only_offset_is_reached = d[ + backtesting.strategy.trailing_stop = d["trailing_stop"] + backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] + backtesting.strategy.trailing_stop_positive_offset = d["trailing_stop_positive_offset"] + backtesting.strategy.trailing_only_offset_is_reached = d[ "trailing_only_offset_is_reached" ] @@ -323,15 +326,15 @@ class HyperOptimizer: self.config.update({"max_open_trades": updated_max_open_trades}) - self.backtesting.strategy.max_open_trades = updated_max_open_trades + backtesting.strategy.max_open_trades = updated_max_open_trades - with self.data_pickle_file.open("rb") as f: + with Path(self.data_pickle_file).open("rb") as f: processed = load(f, mmap_mode="r") - if self.analyze_per_epoch: - # Data is not yet analyzed, rerun populate_indicators. - processed = self.advise_and_trim(processed) + if self.analyze_per_epoch: + # Data is not yet analyzed, rerun populate_indicators. + processed = self.advise_and_trim(processed) - bt_results = self.backtesting.backtest( + bt_results = backtesting.backtest( processed=processed, start_date=self.min_date, end_date=self.max_date ) backtest_end_time = datetime.now(timezone.utc) @@ -341,10 +344,10 @@ class HyperOptimizer: "backtest_end_time": int(backtest_end_time.timestamp()), } ) - - return self._get_results_dict( + result = self._get_results_dict( bt_results, self.min_date, self.max_date, params_dict, processed=processed ) + return result def _get_results_dict( self, @@ -443,7 +446,9 @@ class HyperOptimizer: if isinstance(o_sampler, str): if o_sampler not in optuna_samplers_dict.keys(): raise OperationalException(f"Optuna Sampler {o_sampler} not supported.") - sampler = optuna_samplers_dict[o_sampler](seed=random_state) + with warnings.catch_warnings(): + warnings.filterwarnings(action="ignore", category=ExperimentalWarning) + sampler = optuna_samplers_dict[o_sampler](seed=random_state) else: sampler = o_sampler diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index ad684dd19..e10077f7f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -608,7 +608,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( + hyperopt.hyperopter.backtesting, optimizer_param + ) # list(optimizer_param.values()) assert generate_optimizer_value == response_expected From 1d22377cadfc3b3f885bfff720d9186b1cc39bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 09:58:44 +0200 Subject: [PATCH 16/64] chore: remove some skopt usages --- freqtrade/strategy/parameters.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index f1b025644..636693726 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -16,7 +16,6 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution - # from skopt.space import Integer, Real # Categorical from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException @@ -255,7 +254,7 @@ class RealParameter(NumericParameter): def get_space(self, name: str) -> "ft_FloatDistribution": """ - Create skopt optimization space. + Create optimization space. :param name: A name of parameter field. """ return ft_FloatDistribution(name, self.low, self.high, **self._space_params) @@ -289,7 +288,7 @@ class DecimalParameter(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's NumericParameter. """ self._decimals = decimals default = round(default, self._decimals) @@ -300,7 +299,7 @@ class DecimalParameter(NumericParameter): def get_space(self, name: str) -> "SKDecimal": """ - Create skopt optimization space. + Create optimization space. :param name: A name of parameter field. """ return SKDecimal( From 7a51c9d540a25480df903a75a4159a3065dd0e31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 10:05:11 +0200 Subject: [PATCH 17/64] types: slightly improved typing --- .../optimize/hyperopt/hyperopt_interface.py | 12 +++++----- .../optimize/hyperopt/hyperopt_optimizer.py | 22 +++++++++---------- freqtrade/strategy/parameters.py | 6 ++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index d88a0a27a..9dd656811 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -9,7 +9,6 @@ from abc import ABC from typing import TypeAlias from sklearn.base import RegressorMixin -from skopt.space import Dimension # , Integer, Categorical, from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes @@ -17,6 +16,7 @@ from freqtrade.misc import round_dict from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy from freqtrade.strategy.parameters import ( + DimensionProtocol, ft_CategoricalDistribution, ft_IntDistribution, ) @@ -45,7 +45,7 @@ class IHyperOpt(ABC): # Assign timeframe to be used in hyperopt IHyperOpt.timeframe = str(config["timeframe"]) - def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType: + def generate_estimator(self, dimensions: list[DimensionProtocol], **kwargs) -> EstimatorType: """ Return base_estimator. Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler" @@ -69,7 +69,7 @@ class IHyperOpt(ABC): return roi_table - def roi_space(self) -> list[Dimension]: + def roi_space(self) -> list[DimensionProtocol]: """ Create a ROI space. @@ -154,7 +154,7 @@ class IHyperOpt(ABC): ), ] - def stoploss_space(self) -> list[Dimension]: + def stoploss_space(self) -> list[DimensionProtocol]: """ Create a stoploss space. @@ -178,7 +178,7 @@ class IHyperOpt(ABC): "trailing_only_offset_is_reached": params["trailing_only_offset_is_reached"], } - def trailing_space(self) -> list[Dimension]: + def trailing_space(self) -> list[DimensionProtocol]: """ Create a trailing stoploss space. @@ -204,7 +204,7 @@ class IHyperOpt(ABC): ft_CategoricalDistribution("trailing_only_offset_is_reached", [True, False]), ] - def max_open_trades_space(self) -> list[Dimension]: + def max_open_trades_space(self) -> list[DimensionProtocol]: """ Create a max open trades space. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 19a30015e..e6fb80d66 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -30,6 +30,7 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver +from freqtrade.strategy.parameters import DimensionProtocol from freqtrade.util.dry_run_wallet import get_dry_run_wallet @@ -40,7 +41,6 @@ with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) # warnings.filterwarnings("ignore", category=ExperimentalWarning) import optuna - from skopt.space import Dimension from freqtrade.optimize.space.decimalspace import SKDecimal from freqtrade.strategy.parameters import ( @@ -71,14 +71,14 @@ class HyperOptimizer: """ def __init__(self, config: Config) -> None: - self.buy_space: list[Dimension] = [] - self.sell_space: list[Dimension] = [] - self.protection_space: list[Dimension] = [] - self.roi_space: list[Dimension] = [] - self.stoploss_space: list[Dimension] = [] - self.trailing_space: list[Dimension] = [] - self.max_open_trades_space: list[Dimension] = [] - self.dimensions: list[Dimension] = [] + self.buy_space: list[DimensionProtocol] = [] + self.sell_space: list[DimensionProtocol] = [] + self.protection_space: list[DimensionProtocol] = [] + self.roi_space: list[DimensionProtocol] = [] + self.stoploss_space: list[DimensionProtocol] = [] + self.trailing_space: list[DimensionProtocol] = [] + self.max_open_trades_space: list[DimensionProtocol] = [] + self.dimensions: list[DimensionProtocol] = [] self.o_dimensions: dict = {} self.config = config @@ -146,7 +146,7 @@ class HyperOptimizer: def _get_params_dict( self, - dimensions: list[Dimension], + dimensions: list[DimensionProtocol], raw_params: dict[str, Any], ) -> dict[str, Any]: # logger.info(f"_get_params_dict: {raw_params}") @@ -404,7 +404,7 @@ class HyperOptimizer: "total_profit": total_profit, } - def convert_dimensions_to_optuna_space(self, s_dimensions: list[Dimension]) -> dict: + def convert_dimensions_to_optuna_space(self, s_dimensions: list[DimensionProtocol]) -> dict: o_dimensions: dict[str, optuna.distributions.BaseDistribution] = {} for original_dim in s_dimensions: if isinstance(original_dim, SKDecimal): diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 636693726..a09e5f341 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -7,7 +7,7 @@ import logging from abc import ABC, abstractmethod from collections.abc import Sequence from contextlib import suppress -from typing import Any, Union +from typing import Any, Protocol, Union from freqtrade.enums import HyperoptState from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer @@ -24,6 +24,10 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) +class DimensionProtocol(Protocol): + name: str + + class ft_CategoricalDistribution(CategoricalDistribution): def __init__( self, From 1a2455972976ae802f576ae7ad7e5faf5e814798 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 10:08:41 +0200 Subject: [PATCH 18/64] chore: cleanup old usages --- freqtrade/strategy/parameters.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index a09e5f341..0c049d355 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -207,7 +207,6 @@ class IntParameter(NumericParameter): 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 ft_IntDistribution(name, self.low, self.high, **self._space_params) @property @@ -262,7 +261,6 @@ class RealParameter(NumericParameter): :param name: A name of parameter field. """ return ft_FloatDistribution(name, self.low, self.high, **self._space_params) - # return Real(low=self.low, high=self.high, name=name, **self._space_params) class DecimalParameter(NumericParameter): @@ -365,7 +363,6 @@ class CategoricalParameter(BaseParameter): Create optuna distribution space. :param name: A name of parameter field. """ - # return Categorical(self.opt_range, name=name, **self._space_params) return ft_CategoricalDistribution(name, self.opt_range) @property From 05f19d574a6520f528af1cc4625465924b47b2d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 10:10:48 +0200 Subject: [PATCH 19/64] chore: remove commented skopt usages --- freqtrade/optimize/hyperopt/hyperopt_interface.py | 8 -------- tests/optimize/test_hyperopt.py | 1 - 2 files changed, 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 9dd656811..98b9c806f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -137,11 +137,8 @@ class IHyperOpt(ABC): logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ - # Integer(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], name="roi_t1"), ft_IntDistribution("roi_t1", roi_limits["roi_t1_min"], roi_limits["roi_t1_max"]), - # Integer(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], name="roi_t2"), ft_IntDistribution("roi_t2", roi_limits["roi_t2_min"], roi_limits["roi_t2_max"]), - # Integer(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], name="roi_t3"), ft_IntDistribution("roi_t3", roi_limits["roi_t3_min"], roi_limits["roi_t3_max"]), SKDecimal( roi_limits["roi_p1_min"], roi_limits["roi_p1_max"], decimals=3, name="roi_p1" @@ -191,7 +188,6 @@ class IHyperOpt(ABC): # This parameter is included into the hyperspace dimensions rather than assigning # it explicitly in the code in order to have it printed in the results along with # other 'trailing' hyperspace parameters. - # Categorical([True], name="trailing_stop"), ft_CategoricalDistribution("trailing_stop", [True]), SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', @@ -200,7 +196,6 @@ class IHyperOpt(ABC): # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"), - # Categorical([True, False], name="trailing_only_offset_is_reached"), ft_CategoricalDistribution("trailing_only_offset_is_reached", [True, False]), ] @@ -210,9 +205,6 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ - # return [ - # Integer(-1, 10, name="max_open_trades"), - # ] return [ft_IntDistribution("max_open_trades", -1, 10)] # This is needed for proper unpickling the class attribute timeframe diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e10077f7f..64c00f3a4 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1308,7 +1308,6 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) hyperopt.hyperopter.custom_hyperopt.max_open_trades_space = lambda: [ - # Integer(1, 10, name="max_open_trades") ft_IntDistribution("max_open_trades", 1, 10) ] From 4fcc9dd587769fea4267e4da4472988f88d72382 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 12:10:48 +0200 Subject: [PATCH 20/64] feat: use floatDistribution for SKDecimal --- .../optimize/hyperopt/hyperopt_optimizer.py | 15 +---- freqtrade/optimize/space/decimalspace.py | 57 ++++++------------- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index e6fb80d66..b5d495228 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -407,18 +407,9 @@ class HyperOptimizer: def convert_dimensions_to_optuna_space(self, s_dimensions: list[DimensionProtocol]) -> dict: o_dimensions: dict[str, optuna.distributions.BaseDistribution] = {} for original_dim in s_dimensions: - if isinstance(original_dim, SKDecimal): - o_dimensions[original_dim.name] = optuna.distributions.FloatDistribution( - original_dim.low_orig, - original_dim.high_orig, - log=False, - step=1 / pow(10, original_dim.decimals), - ) - # for preparing to remove old skopt spaces - elif ( - isinstance(original_dim, ft_CategoricalDistribution) - or isinstance(original_dim, ft_IntDistribution) - or isinstance(original_dim, ft_FloatDistribution) + if isinstance( + original_dim, + ft_CategoricalDistribution | ft_IntDistribution | ft_FloatDistribution | SKDecimal, ): o_dimensions[original_dim.name] = original_dim else: diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index f5c122fb3..2a46f2d33 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,47 +1,26 @@ -import numpy as np -from skopt.space import Integer +from optuna.distributions import FloatDistribution -class SKDecimal(Integer): +class SKDecimal(FloatDistribution): def __init__( self, - low, - high, - decimals=3, - prior="uniform", - base=10, - transform=None, + low: float, + high: float, + step: float | None = None, + decimals: int | None = 3, name=None, - dtype=np.int64, ): - self.decimals = decimals + """ + FloatDistribution with a fixed step size. + """ + if decimals is not None and step is not None: + raise ValueError("You can only set one of decimals or step") + # Convert decimals to step + self.step = step or 1 / 10**decimals + self.name = name - self.pow_dot_one = pow(0.1, self.decimals) - self.pow_ten = pow(10, self.decimals) - - _low = int(low * self.pow_ten) - _high = int(high * self.pow_ten) - # trunc to precision to avoid points out of space - self.low_orig = round(_low * self.pow_dot_one, self.decimals) - self.high_orig = round(_high * self.pow_dot_one, self.decimals) - - super().__init__(_low, _high, prior, base, transform, name, dtype) - - def __repr__(self): - return ( - f"Decimal(low={self.low_orig}, high={self.high_orig}, decimals={self.decimals}, " - f"prior='{self.prior}', transform='{self.transform_}')" + super().__init__( + low=low, + high=high, + step=self.step, ) - - def __contains__(self, point): - if isinstance(point, list): - point = np.array(point) - return self.low_orig <= point <= self.high_orig - - def transform(self, Xt): - return super().transform([int(v * self.pow_ten) for v in Xt]) - - def inverse_transform(self, Xt): - res = super().inverse_transform(Xt) - # equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res] - return [int(v) / self.pow_ten for v in res] From 85689ebc1c52ede99cce63bb3cbdfe04c9e8186a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Apr 2025 12:37:17 +0200 Subject: [PATCH 21/64] test: update skdecimal test to use optuna --- tests/optimize/test_hyperopt.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 64c00f3a4..91635857b 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1190,19 +1190,16 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fe 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._contains(1.5) + assert not space._contains(2.5) + assert space.low == 1 + assert space.high == 2 - 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] + assert space._contains(1.51) + assert space._contains(1.01) + # Falls out of the space with 2 decimals + assert not space._contains(1.511) + assert not space._contains(1.111222) def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, fee) -> None: From 35c3868c560228c2f1384061427bbb5b2206f8a7 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 08:58:42 +0300 Subject: [PATCH 22/64] change for SKDecimal and othercomments --- .../optimize/hyperopt/hyperopt_interface.py | 16 ++-- .../optimize/hyperopt/hyperopt_optimizer.py | 4 +- freqtrade/optimize/space/__init__.py | 7 +- freqtrade/optimize/space/decimalspace.py | 5 +- freqtrade/strategy/parameters.py | 77 ++++++++++--------- tests/optimize/test_hyperopt.py | 6 +- 6 files changed, 61 insertions(+), 54 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 98b9c806f..61565910f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -14,12 +14,12 @@ from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.optimize.space import SKDecimal -from freqtrade.strategy import IStrategy -from freqtrade.strategy.parameters import ( +from freqtrade.optimize.space.optunaspaces import ( DimensionProtocol, ft_CategoricalDistribution, ft_IntDistribution, ) +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -137,9 +137,9 @@ class IHyperOpt(ABC): logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ - ft_IntDistribution("roi_t1", roi_limits["roi_t1_min"], roi_limits["roi_t1_max"]), - ft_IntDistribution("roi_t2", roi_limits["roi_t2_min"], roi_limits["roi_t2_max"]), - ft_IntDistribution("roi_t3", roi_limits["roi_t3_min"], roi_limits["roi_t3_max"]), + ft_IntDistribution(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], "roi_t1"), + ft_IntDistribution(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], "roi_t2"), + ft_IntDistribution(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], "roi_t3"), SKDecimal( roi_limits["roi_p1_min"], roi_limits["roi_p1_max"], decimals=3, name="roi_p1" ), @@ -188,7 +188,7 @@ class IHyperOpt(ABC): # This parameter is included into the hyperspace dimensions rather than assigning # it explicitly in the code in order to have it printed in the results along with # other 'trailing' hyperspace parameters. - ft_CategoricalDistribution("trailing_stop", [True]), + ft_CategoricalDistribution([True], "trailing_stop"), SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between @@ -196,7 +196,7 @@ class IHyperOpt(ABC): # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"), - ft_CategoricalDistribution("trailing_only_offset_is_reached", [True, False]), + ft_CategoricalDistribution([True, False], "trailing_only_offset_is_reached"), ] def max_open_trades_space(self) -> list[DimensionProtocol]: @@ -205,7 +205,7 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ - return [ft_IntDistribution("max_open_trades", -1, 10)] + return [ft_IntDistribution(-1, 10, "max_open_trades")] # This is needed for proper unpickling the class attribute timeframe # which is set to the actual value by the resolver. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index b5d495228..3105d5f1c 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -30,7 +30,6 @@ from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLo from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver -from freqtrade.strategy.parameters import DimensionProtocol from freqtrade.util.dry_run_wallet import get_dry_run_wallet @@ -43,7 +42,8 @@ with warnings.catch_warnings(): import optuna from freqtrade.optimize.space.decimalspace import SKDecimal - from freqtrade.strategy.parameters import ( + from freqtrade.optimize.space.optunaspaces import ( + DimensionProtocol, ft_CategoricalDistribution, ft_FloatDistribution, ft_IntDistribution, diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index 6c59a4d8f..2e81a5e4b 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,3 +1,6 @@ -from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401 - +# from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401 from .decimalspace import SKDecimal # noqa: F401 +from .optunaspaces import DimensionProtocol # noqa: F401 +from .optunaspaces import ft_CategoricalDistribution as Categorical # noqa: F401 +from .optunaspaces import ft_FloatDistribution as Real # noqa: F401 +from .optunaspaces import ft_IntDistribution as Integer # noqa: F401 diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index 2a46f2d33..63ae38aad 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from optuna.distributions import FloatDistribution @@ -7,7 +9,7 @@ class SKDecimal(FloatDistribution): low: float, high: float, step: float | None = None, - decimals: int | None = 3, + decimals: int = 4, name=None, ): """ @@ -19,6 +21,7 @@ class SKDecimal(FloatDistribution): self.step = step or 1 / 10**decimals self.name = name + # with localcontext(prec=max(decimals, 4)) as ctx: super().__init__( low=low, high=high, diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 0c049d355..f738ca526 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -7,16 +7,19 @@ import logging from abc import ABC, abstractmethod from collections.abc import Sequence from contextlib import suppress -from typing import Any, Protocol, Union +from typing import Any, Union from freqtrade.enums import HyperoptState from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): - from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution - from freqtrade.optimize.space import SKDecimal + from freqtrade.optimize.space.optunaspaces import ( + ft_CategoricalDistribution, + ft_FloatDistribution, + ft_IntDistribution, + ) from freqtrade.exceptions import OperationalException @@ -24,43 +27,43 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -class DimensionProtocol(Protocol): - name: str +# class DimensionProtocol(Protocol): +# name: str -class ft_CategoricalDistribution(CategoricalDistribution): - def __init__( - self, - name: str, - categories: Sequence[Any], - **kwargs, - ): - self.name = name - return super().__init__(categories) +# class ft_CategoricalDistribution(CategoricalDistribution): +# def __init__( +# self, +# name: str, +# categories: Sequence[Any], +# **kwargs, +# ): +# self.name = name +# return super().__init__(categories) -class ft_IntDistribution(IntDistribution): - def __init__( - self, - name: str, - low: int | float, - high: int | float, - **kwargs, - ): - self.name = name - return super().__init__(int(low), int(high), **kwargs) +# class ft_IntDistribution(IntDistribution): +# def __init__( +# self, +# name: str, +# low: int | float, +# high: int | float, +# **kwargs, +# ): +# self.name = name +# return super().__init__(int(low), int(high), **kwargs) -class ft_FloatDistribution(FloatDistribution): - def __init__( - self, - name: str, - low: float, - high: float, - **kwargs, - ): - self.name = name - return super().__init__(low, high, **kwargs) +# class ft_FloatDistribution(FloatDistribution): +# def __init__( +# self, +# name: str, +# low: float, +# high: float, +# **kwargs, +# ): +# self.name = name +# return super().__init__(low, high, **kwargs) class BaseParameter(ABC): @@ -207,7 +210,7 @@ class IntParameter(NumericParameter): Create optuna distribution space. :param name: A name of parameter field. """ - return ft_IntDistribution(name, self.low, self.high, **self._space_params) + return ft_IntDistribution(self.low, self.high, name, **self._space_params) @property def range(self): @@ -260,7 +263,7 @@ class RealParameter(NumericParameter): Create optimization space. :param name: A name of parameter field. """ - return ft_FloatDistribution(name, self.low, self.high, **self._space_params) + return ft_FloatDistribution(self.low, self.high, name, **self._space_params) class DecimalParameter(NumericParameter): @@ -363,7 +366,7 @@ class CategoricalParameter(BaseParameter): Create optuna distribution space. :param name: A name of parameter field. """ - return ft_CategoricalDistribution(name, self.opt_range) + return ft_CategoricalDistribution(self.opt_range, name) @property def range(self): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 91635857b..9d5a6f918 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -17,10 +17,8 @@ from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal +from freqtrade.optimize.space.optunaspaces import ft_IntDistribution from freqtrade.strategy import IntParameter - -# from skopt.space import Integer -from freqtrade.strategy.parameters import ft_IntDistribution from freqtrade.util import dt_utc from tests.conftest import ( CURRENT_TEST_STRATEGY, @@ -1305,7 +1303,7 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) hyperopt.hyperopter.custom_hyperopt.max_open_trades_space = lambda: [ - ft_IntDistribution("max_open_trades", 1, 10) + ft_IntDistribution(1, 10, "max_open_trades") ] first_time_evaluated = False From c89058788e2511b8c1335af6c3cdad4dcae06d1e Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 09:49:33 +0300 Subject: [PATCH 23/64] remove decimal module --- freqtrade/optimize/space/__init__.py | 2 +- freqtrade/optimize/space/decimalspace.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index 2e81a5e4b..08abe3097 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,6 +1,6 @@ # from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401 from .decimalspace import SKDecimal # noqa: F401 -from .optunaspaces import DimensionProtocol # noqa: F401 +from .optunaspaces import DimensionProtocol as Dimension # noqa: F401 from .optunaspaces import ft_CategoricalDistribution as Categorical # noqa: F401 from .optunaspaces import ft_FloatDistribution as Real # noqa: F401 from .optunaspaces import ft_IntDistribution as Integer # noqa: F401 diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index 63ae38aad..c93e49c01 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,5 +1,3 @@ -from decimal import Decimal - from optuna.distributions import FloatDistribution @@ -9,7 +7,7 @@ class SKDecimal(FloatDistribution): low: float, high: float, step: float | None = None, - decimals: int = 4, + decimals: int = 3, name=None, ): """ @@ -21,7 +19,6 @@ class SKDecimal(FloatDistribution): self.step = step or 1 / 10**decimals self.name = name - # with localcontext(prec=max(decimals, 4)) as ctx: super().__init__( low=low, high=high, From 5c859d929b640523b73ffaf2691cf2562993e1c5 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 11:06:39 +0300 Subject: [PATCH 24/64] add optunaspaces.py --- freqtrade/optimize/space/optunaspaces.py | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 freqtrade/optimize/space/optunaspaces.py diff --git a/freqtrade/optimize/space/optunaspaces.py b/freqtrade/optimize/space/optunaspaces.py new file mode 100644 index 000000000..d574afe0f --- /dev/null +++ b/freqtrade/optimize/space/optunaspaces.py @@ -0,0 +1,59 @@ +from collections.abc import Sequence +from typing import Any, Protocol + +from optuna.distributions import CategoricalDistribution, FloatDistribution, IntDistribution + + +class DimensionProtocol(Protocol): + name: str + + +class ft_CategoricalDistribution(CategoricalDistribution): + def __init__( + self, + categories: Sequence[Any], + name: str, + **kwargs, + ): + self.name = name + self.categories = categories + # if len(categories) <= 1: + # raise Exception(f"need at least 2 categories for {name}") + return super().__init__(categories) + + def __repr__(self): + return f"CategoricalDistribution({self.categories})" + + +class ft_IntDistribution(IntDistribution): + def __init__( + self, + low: int | float, + high: int | float, + name: str, + **kwargs, + ): + self.name = name + self.low = low + self.high = high + return super().__init__(int(low), int(high), **kwargs) + + def __repr__(self): + return f"IntDistribution(low={self.low}, high={self.high})" + + +class ft_FloatDistribution(FloatDistribution): + def __init__( + self, + low: float, + high: float, + name: str, + **kwargs, + ): + self.name = name + self.low = low + self.high = high + return super().__init__(low, high, **kwargs) + + def __repr__(self): + return f"FloatDistribution(low={self.low}, high={self.high}, step={self.step})" From 30ead79e112e5c33afa873ad332b2a033c74efe4 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 11:22:07 +0300 Subject: [PATCH 25/64] fixed freqtrade/optimize/space/__init__.py:1:66: RUF100 [*] Unused `noqa` directive (unused: `F401`) --- freqtrade/optimize/space/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index 08abe3097..0a16bf8af 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,4 +1,3 @@ -# from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401 from .decimalspace import SKDecimal # noqa: F401 from .optunaspaces import DimensionProtocol as Dimension # noqa: F401 from .optunaspaces import ft_CategoricalDistribution as Categorical # noqa: F401 From 8ee40ade458a448ea94f78562b092faac4e4f241 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 11:27:33 +0300 Subject: [PATCH 26/64] update docs/advanced-hyperopt.md --- docs/advanced-hyperopt.md | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index e12d700f3..97d6ed9ba 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -167,7 +167,7 @@ You can define your own optuna sampler for Hyperopt by implementing `generate_es class MyAwesomeStrategy(IStrategy): class HyperOpt: def generate_estimator(dimensions: List['Dimension'], **kwargs): - return "TPESampler" + return "NSGAIIISampler" ``` @@ -175,32 +175,10 @@ Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "C Some research will be necessary to find additional Samplers (from optunahub) for example. -``` - -The `dimensions` parameter is the list of `skopt.space.Dimension` objects corresponding to the parameters to be optimized. It can be used to create isotropic kernels for the `skopt.learning.GaussianProcessRegressor` estimator. Here's an example: - -```python -class MyAwesomeStrategy(IStrategy): - class HyperOpt: - def generate_estimator(dimensions: List['Dimension'], **kwargs): - from skopt.utils import cook_estimator - from skopt.learning.gaussian_process.kernels import (Matern, ConstantKernel) - kernel_bounds = (0.0001, 10000) - kernel = ( - ConstantKernel(1.0, kernel_bounds) * - Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=2.5) - ) - kernel += ( - ConstantKernel(1.0, kernel_bounds) * - Matern(length_scale=np.ones(len(dimensions)), length_scale_bounds=[kernel_bounds for d in dimensions], nu=1.5) - ) - - return cook_estimator("GP", space=dimensions, kernel=kernel, n_restarts_optimizer=2) -``` !!! Note While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. - If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters. + If you're unsure about this, best use one of the Defaults (`"NSGAIIISampler"` has proven to be the most versatile) without further parameters. ## Space options From 20fca07d8f9645652b9e08bc5fda220e41b16b34 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 13 Apr 2025 12:39:44 +0300 Subject: [PATCH 27/64] fixed mypy errors freqtrade/optimize/space/optunaspaces.py:39: error: Argument 1 to "__init__" of "IntDistribution" has incompatible type "int | float"; expected "int" [arg-type] freqtrade/optimize/space/optunaspaces.py:39: error: Argument 2 to "__init__" of "IntDistribution" has incompatible type "int | float"; expected "int" [arg-type] remove all references for ExtraTreesRegressor and skopt.space --- docs/hyperopt.md | 2 +- freqtrade/optimize/hyperopt/hyperopt_auto.py | 2 +- freqtrade/optimize/space/optunaspaces.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51a6a5187..fdde74fb6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -4,7 +4,7 @@ This page explains how to tune your strategy by finding the optimal parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this. The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. -In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses Bayesian search with a ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). +In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses one of optuna's sampler algorithms (currently NSGAIIISampler) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). Hyperopt requires historic data to be available, just as backtesting does (hyperopt runs backtesting many times with different parameters). To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 1da80ec02..6fabaaf35 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException with suppress(ImportError): - from skopt.space import Dimension + from freqtrade.optimize.space import Dimension from freqtrade.optimize.hyperopt.hyperopt_interface import EstimatorType, IHyperOpt diff --git a/freqtrade/optimize/space/optunaspaces.py b/freqtrade/optimize/space/optunaspaces.py index d574afe0f..9f152b991 100644 --- a/freqtrade/optimize/space/optunaspaces.py +++ b/freqtrade/optimize/space/optunaspaces.py @@ -34,9 +34,9 @@ class ft_IntDistribution(IntDistribution): **kwargs, ): self.name = name - self.low = low - self.high = high - return super().__init__(int(low), int(high), **kwargs) + self.low = int(low) + self.high = int(high) + return super().__init__(self.low, self.high, **kwargs) def __repr__(self): return f"IntDistribution(low={self.low}, high={self.high})" From 8a9b31eccd998224951c3c950290d8d3c82db843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:15:16 +0200 Subject: [PATCH 28/64] chore: remove scikit-optimize dependency --- docs/hyperopt.md | 2 +- pyproject.toml | 1 - requirements-hyperopt.txt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fdde74fb6..b8c3cc7ef 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -1,7 +1,7 @@ # Hyperopt This page explains how to tune your strategy by finding the optimal -parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `scikit-optimize` package to accomplish this. +parameters, a process called hyperparameter optimization. The bot uses algorithms included in the `optuna` package to accomplish this. The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. In general, the search for best parameters starts with a few random combinations (see [below](#reproducible-results) for more details) and then uses one of optuna's sampler algorithms (currently NSGAIIISampler) to quickly find a combination of parameters in the search hyperspace that minimizes the value of the [loss function](#loss-functions). diff --git a/pyproject.toml b/pyproject.toml index 8ee9818b8..bf40c456e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,6 @@ plot = ["plotly>=4.0"] hyperopt = [ "scipy", "scikit-learn", - "ft-scikit-optimize>=0.9.2", "filelock", ] freqai = [ diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index cb98d6bb7..58a9d1b83 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -4,7 +4,6 @@ # Required for hyperopt scipy==1.15.2 scikit-learn==1.6.1 -ft-scikit-optimize==0.9.2 filelock==3.18.0 optuna==4.2.1 cmaes==0.11.1 From 8af9875d45b08ed6a6c6078463ea04f7d0e65049 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:17:43 +0200 Subject: [PATCH 29/64] docs: remove scikit-optimize references from docs --- docs/advanced-hyperopt.md | 1 - docs/faq.md | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 97d6ed9ba..5b7092f51 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -175,7 +175,6 @@ Possible values are either one of "NSGAIISampler", "TPESampler", "GPSampler", "C Some research will be necessary to find additional Samplers (from optunahub) for example. - !!! Note While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. If you're unsure about this, best use one of the Defaults (`"NSGAIIISampler"` has proven to be the most versatile) without further parameters. diff --git a/docs/faq.md b/docs/faq.md index 8d19b5db0..e0be88d14 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -219,10 +219,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations. The GPU improvements would only apply to pandas-native calculations - or ones written by yourself. -For hyperopt, freqtrade is using scikit-optimize, which is built on top of scikit-learn. -Their statement about GPU support is [pretty clear](https://scikit-learn.org/stable/faq.html#will-you-add-gpu-support). - -GPU's also are only good at crunching numbers (floating point operations). +GPU's are only good at crunching numbers (floating point operations). For hyperopt, we need both number-crunching (find next parameters) and running python code (running backtesting). As such, GPU's are not too well suited for most parts of hyperopt. From f86bc71c43bbe497582f8302914f6db604f3854d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:19:55 +0200 Subject: [PATCH 30/64] chore: cleanup some test code --- tests/optimize/test_hyperopt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 9d5a6f918..83ce63c6f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -609,8 +609,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( hyperopt.hyperopter.backtesting, optimizer_param ) - # list(optimizer_param.values()) - assert generate_optimizer_value == response_expected @@ -1312,7 +1310,7 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No @wraps(func) def wrapper(*args, **kwargs): nonlocal first_time_evaluated - print(f"max_open_trades: {hyperopt.hyperopter.backtesting.strategy.max_open_trades}") + stake_amount = func(*args, **kwargs) if first_time_evaluated is False: assert stake_amount == 2 From f5451138402a649b46170f4d27ace8892b3e912d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:33:51 +0200 Subject: [PATCH 31/64] chore: improve EstimatorType type --- freqtrade/optimize/hyperopt/hyperopt_interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 61565910f..c09c956ef 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -8,7 +8,7 @@ import math from abc import ABC from typing import TypeAlias -from sklearn.base import RegressorMixin +from optuna.samplers import BaseSampler from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes @@ -24,7 +24,7 @@ from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) -EstimatorType: TypeAlias = RegressorMixin | str +EstimatorType: TypeAlias = BaseSampler | str class IHyperOpt(ABC): @@ -188,7 +188,7 @@ class IHyperOpt(ABC): # This parameter is included into the hyperspace dimensions rather than assigning # it explicitly in the code in order to have it printed in the results along with # other 'trailing' hyperspace parameters. - ft_CategoricalDistribution([True], "trailing_stop"), + ft_CategoricalDistribution([True], name="trailing_stop"), SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between @@ -196,7 +196,7 @@ class IHyperOpt(ABC): # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"), - ft_CategoricalDistribution([True, False], "trailing_only_offset_is_reached"), + ft_CategoricalDistribution([True, False], name="trailing_only_offset_is_reached"), ] def max_open_trades_space(self) -> list[DimensionProtocol]: @@ -205,7 +205,9 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ - return [ft_IntDistribution(-1, 10, "max_open_trades")] + return [ + ft_IntDistribution(-1, 10, name="max_open_trades"), + ] # This is needed for proper unpickling the class attribute timeframe # which is set to the actual value by the resolver. From 83cdf76636e05c5c198ac4914c028dd2eba32d1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:37:54 +0200 Subject: [PATCH 32/64] chore: simplify import/exports --- .../optimize/hyperopt/hyperopt_interface.py | 4 +-- .../optimize/hyperopt/hyperopt_optimizer.py | 4 +-- freqtrade/optimize/space/__init__.py | 27 +++++++++++++++---- freqtrade/strategy/parameters.py | 4 +-- tests/optimize/test_hyperopt.py | 3 +-- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index c09c956ef..093b9225f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -13,9 +13,9 @@ from optuna.samplers import BaseSampler from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.optimize.space import SKDecimal -from freqtrade.optimize.space.optunaspaces import ( +from freqtrade.optimize.space import ( DimensionProtocol, + SKDecimal, ft_CategoricalDistribution, ft_IntDistribution, ) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 3105d5f1c..e3fe1369d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -41,9 +41,9 @@ with warnings.catch_warnings(): # warnings.filterwarnings("ignore", category=ExperimentalWarning) import optuna - from freqtrade.optimize.space.decimalspace import SKDecimal - from freqtrade.optimize.space.optunaspaces import ( + from freqtrade.optimize.space import ( DimensionProtocol, + SKDecimal, ft_CategoricalDistribution, ft_FloatDistribution, ft_IntDistribution, diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index 0a16bf8af..77552b9f2 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,5 +1,22 @@ -from .decimalspace import SKDecimal # noqa: F401 -from .optunaspaces import DimensionProtocol as Dimension # noqa: F401 -from .optunaspaces import ft_CategoricalDistribution as Categorical # noqa: F401 -from .optunaspaces import ft_FloatDistribution as Real # noqa: F401 -from .optunaspaces import ft_IntDistribution as Integer # noqa: F401 +from .decimalspace import SKDecimal +from .optunaspaces import ( + DimensionProtocol, + ft_CategoricalDistribution, + ft_FloatDistribution, + ft_IntDistribution, +) + + +# Alias for the distribution classes +Dimension = DimensionProtocol +Categorical = ft_CategoricalDistribution +Integer = ft_IntDistribution +Real = ft_FloatDistribution + +__all__ = [ + "Categorical", + "Dimension", + "Integer", + "Real", + "SKDecimal", +] diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index f738ca526..31e1d075d 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -14,8 +14,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): - from freqtrade.optimize.space import SKDecimal - from freqtrade.optimize.space.optunaspaces import ( + from freqtrade.optimize.space import ( + SKDecimal, ft_CategoricalDistribution, ft_FloatDistribution, ft_IntDistribution, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 83ce63c6f..c1497f2b2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -16,8 +16,7 @@ from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.optimize.space import SKDecimal -from freqtrade.optimize.space.optunaspaces import ft_IntDistribution +from freqtrade.optimize.space import SKDecimal, ft_IntDistribution from freqtrade.strategy import IntParameter from freqtrade.util import dt_utc from tests.conftest import ( From 9b08b51ad85eb5d328b564d356120fbdc55956ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:40:03 +0200 Subject: [PATCH 33/64] chore: cleanup dead code --- freqtrade/strategy/parameters.py | 39 -------------------------------- 1 file changed, 39 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 31e1d075d..4072b8490 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -27,45 +27,6 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -# class DimensionProtocol(Protocol): -# name: str - - -# class ft_CategoricalDistribution(CategoricalDistribution): -# def __init__( -# self, -# name: str, -# categories: Sequence[Any], -# **kwargs, -# ): -# self.name = name -# return super().__init__(categories) - - -# class ft_IntDistribution(IntDistribution): -# def __init__( -# self, -# name: str, -# low: int | float, -# high: int | float, -# **kwargs, -# ): -# self.name = name -# return super().__init__(int(low), int(high), **kwargs) - - -# class ft_FloatDistribution(FloatDistribution): -# def __init__( -# self, -# name: str, -# low: float, -# high: float, -# **kwargs, -# ): -# self.name = name -# return super().__init__(low, high, **kwargs) - - class BaseParameter(ABC): """ Defines a parameter that can be optimized by hyperopt. From 2abf22e37ba6c884858e98b8ea472c3eca58f094 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:43:10 +0200 Subject: [PATCH 34/64] chore: simplify usage of data_pickle_file --- freqtrade/optimize/hyperopt/hyperopt.py | 3 +-- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 5c906cb9a..afaff8593 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -94,8 +94,7 @@ class Hyperopt: self.hyperopt_table_header = 0 self.print_json = self.config.get("print_json", False) - self.hyperopter = HyperOptimizer(self.config) - self.hyperopter.data_pickle_file = self.data_pickle_file + self.hyperopter = HyperOptimizer(self.config, self.data_pickle_file) @staticmethod def get_lock_filename(config: Config) -> str: diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index e3fe1369d..d4af83387 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -70,7 +70,7 @@ class HyperOptimizer: This class is sent to the hyperopt worker processes. """ - def __init__(self, config: Config) -> None: + def __init__(self, config: Config, data_pickle_file: Path) -> None: self.buy_space: list[DimensionProtocol] = [] self.sell_space: list[DimensionProtocol] = [] self.protection_space: list[DimensionProtocol] = [] @@ -106,8 +106,10 @@ class HyperOptimizer: self.config ) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + + self.data_pickle_file = data_pickle_file + self.market_change = 0.0 - self.data_pickle_file = "" if HyperoptTools.has_space(self.config, "sell"): # Make sure use_exit_signal is enabled From ed22789a1aea653f449a23267369ddd956224829 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 19:53:07 +0200 Subject: [PATCH 35/64] chore: cleanup unused import --- freqtrade/optimize/hyperopt/hyperopt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index afaff8593..174cfa103 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -32,9 +32,6 @@ from freqtrade.optimize.hyperopt_tools import ( from freqtrade.util import get_progress_tracker -# import multiprocessing as mp -# mp.set_start_method('fork', force=True) # spawn fork forkserver - logger = logging.getLogger(__name__) From 057cc2538e00d232402eadc104bdd4acf7ed574e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 20:00:19 +0200 Subject: [PATCH 36/64] chore: use optuna distribution aliases in parameters --- freqtrade/strategy/parameters.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 4072b8490..1b840cdb4 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -15,10 +15,10 @@ from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer with suppress(ImportError): from freqtrade.optimize.space import ( + Categorical, + Integer, + Real, SKDecimal, - ft_CategoricalDistribution, - ft_FloatDistribution, - ft_IntDistribution, ) from freqtrade.exceptions import OperationalException @@ -71,11 +71,7 @@ class BaseParameter(ABC): return f"{self.__class__.__name__}({self.value})" @abstractmethod - def get_space( - self, name: str - ) -> Union[ - "ft_IntDistribution", "ft_FloatDistribution", "SKDecimal", "ft_CategoricalDistribution" - ]: + def get_space(self, name: str) -> Union["Integer", "Real", "SKDecimal", "Categorical"]: """ Get-space - will be used by Hyperopt to get the hyperopt Space """ @@ -166,12 +162,12 @@ class IntParameter(NumericParameter): low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs ) - def get_space(self, name: str) -> "ft_IntDistribution": + def get_space(self, name: str) -> "Integer": """ Create optuna distribution space. :param name: A name of parameter field. """ - return ft_IntDistribution(self.low, self.high, name, **self._space_params) + return Integer(low=self.low, high=self.high, name=name, **self._space_params) @property def range(self): @@ -219,12 +215,12 @@ class RealParameter(NumericParameter): low=low, high=high, default=default, space=space, optimize=optimize, load=load, **kwargs ) - def get_space(self, name: str) -> "ft_FloatDistribution": + def get_space(self, name: str) -> "Real": """ Create optimization space. :param name: A name of parameter field. """ - return ft_FloatDistribution(self.low, self.high, name, **self._space_params) + return Real(low=self.low, high=self.high, name=name, **self._space_params) class DecimalParameter(NumericParameter): @@ -313,7 +309,8 @@ 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 optuna.distributions.CategoricalDistribution. + :param kwargs: Compatibility. Optuna's CategoricalDistribution does not + accept extra parameters """ if len(categories) < 2: raise OperationalException( @@ -322,12 +319,12 @@ class CategoricalParameter(BaseParameter): self.opt_range = categories super().__init__(default=default, space=space, optimize=optimize, load=load, **kwargs) - def get_space(self, name: str) -> "ft_CategoricalDistribution": + def get_space(self, name: str) -> "Categorical": """ Create optuna distribution space. :param name: A name of parameter field. """ - return ft_CategoricalDistribution(self.opt_range, name) + return Categorical(self.opt_range, name=name) @property def range(self): From ca5ccc8799ba2b20a540a64ae39f8bef0f8b3984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 20:07:40 +0200 Subject: [PATCH 37/64] chore: cleanup some code --- freqtrade/optimize/hyperopt/hyperopt.py | 3 --- .../optimize/hyperopt/hyperopt_optimizer.py | 21 +++---------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 174cfa103..6aeb79057 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -306,8 +306,6 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=current_jobs, dimensions=self.hyperopter.o_dimensions ) - # asked_params = [asked1.params for asked1 in asked] - # logger.info(f"asked iteration {i}: {asked} {asked_params}") f_val = self.run_optimizer_parallel( parallel, @@ -317,7 +315,6 @@ class Hyperopt: f_val_loss = [v["loss"] for v in f_val] for o_ask, v in zip(asked, f_val_loss, strict=False): self.opt.tell(o_ask, v) - # logger.info(f"result iteration {i}: {asked} {f_val_loss}") for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index d4af83387..0a616927d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -147,11 +147,8 @@ class HyperOptimizer: self.hyperopt_pickle_magic(modules.__bases__) def _get_params_dict( - self, - dimensions: list[DimensionProtocol], - raw_params: dict[str, Any], + self, dimensions: list[DimensionProtocol], raw_params: dict[str, Any] ) -> dict[str, Any]: - # logger.info(f"_get_params_dict: {raw_params}") # Ensure the number of dimensions match # the number of parameters in the list. if len(raw_params) != len(dimensions): @@ -159,9 +156,6 @@ class HyperOptimizer: # Return a dict where the keys are the names of the dimensions # and the values are taken from the list of parameters. - # result = {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} - # logger.info(f"d_get_params_dict: {result}") - # return {d.name: v for d, v in zip(dimensions, raw_params.params, strict=False)} return raw_params def _get_params_details(self, params: dict) -> dict: @@ -273,12 +267,9 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - # @profile - # fp=open('memory_profiler.log','w+') - # @profile(stream=fp) def generate_optimizer( self, backtesting: Backtesting, raw_params: dict[str, Any] - ) -> dict[str, Any]: # list[Any] + ) -> dict[str, Any]: """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -330,7 +321,7 @@ class HyperOptimizer: backtesting.strategy.max_open_trades = updated_max_open_trades - with Path(self.data_pickle_file).open("rb") as f: + with self.data_pickle_file.open("rb") as f: processed = load(f, mmap_mode="r") if self.analyze_per_epoch: # Data is not yet analyzed, rerun populate_indicators. @@ -419,7 +410,6 @@ class HyperOptimizer: f"Unknown search space {original_dim.name} - {original_dim} / \ {type(original_dim)}" ) - # logger.info(f"convert_dimensions_to_optuna_space: {s_dimensions} - {o_dimensions}") return o_dimensions def get_optimizer( @@ -431,11 +421,6 @@ class HyperOptimizer: ) self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) - # for save/restore - # with open("sampler.pkl", "wb") as fout: - # pickle.dump(study.sampler, fout) - # restored_sampler = pickle.load(open("sampler.pkl", "rb")) - if isinstance(o_sampler, str): if o_sampler not in optuna_samplers_dict.keys(): raise OperationalException(f"Optuna Sampler {o_sampler} not supported.") From 3fc40f45b3bb8ec03a223f3a9a169ba70e43d023 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 20:15:24 +0200 Subject: [PATCH 38/64] chore: simplify diff in hyperopt-tinterface Use aliases where possible. --- .../optimize/hyperopt/hyperopt_interface.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py index 093b9225f..76f8907f9 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt/hyperopt_interface.py @@ -13,12 +13,7 @@ from optuna.samplers import BaseSampler from freqtrade.constants import Config from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict -from freqtrade.optimize.space import ( - DimensionProtocol, - SKDecimal, - ft_CategoricalDistribution, - ft_IntDistribution, -) +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal from freqtrade.strategy import IStrategy @@ -45,7 +40,7 @@ class IHyperOpt(ABC): # Assign timeframe to be used in hyperopt IHyperOpt.timeframe = str(config["timeframe"]) - def generate_estimator(self, dimensions: list[DimensionProtocol], **kwargs) -> EstimatorType: + def generate_estimator(self, dimensions: list[Dimension], **kwargs) -> EstimatorType: """ Return base_estimator. Can be any of "TPESampler", "GPSampler", "CmaEsSampler", "NSGAIISampler" @@ -69,7 +64,7 @@ class IHyperOpt(ABC): return roi_table - def roi_space(self) -> list[DimensionProtocol]: + def roi_space(self) -> list[Dimension]: """ Create a ROI space. @@ -137,9 +132,9 @@ class IHyperOpt(ABC): logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ - ft_IntDistribution(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], "roi_t1"), - ft_IntDistribution(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], "roi_t2"), - ft_IntDistribution(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], "roi_t3"), + Integer(roi_limits["roi_t1_min"], roi_limits["roi_t1_max"], name="roi_t1"), + Integer(roi_limits["roi_t2_min"], roi_limits["roi_t2_max"], name="roi_t2"), + Integer(roi_limits["roi_t3_min"], roi_limits["roi_t3_max"], name="roi_t3"), SKDecimal( roi_limits["roi_p1_min"], roi_limits["roi_p1_max"], decimals=3, name="roi_p1" ), @@ -151,7 +146,7 @@ class IHyperOpt(ABC): ), ] - def stoploss_space(self) -> list[DimensionProtocol]: + def stoploss_space(self) -> list[Dimension]: """ Create a stoploss space. @@ -175,7 +170,7 @@ class IHyperOpt(ABC): "trailing_only_offset_is_reached": params["trailing_only_offset_is_reached"], } - def trailing_space(self) -> list[DimensionProtocol]: + def trailing_space(self) -> list[Dimension]: """ Create a trailing stoploss space. @@ -188,7 +183,7 @@ class IHyperOpt(ABC): # This parameter is included into the hyperspace dimensions rather than assigning # it explicitly in the code in order to have it printed in the results along with # other 'trailing' hyperspace parameters. - ft_CategoricalDistribution([True], name="trailing_stop"), + Categorical([True], name="trailing_stop"), SKDecimal(0.01, 0.35, decimals=3, name="trailing_stop_positive"), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between @@ -196,17 +191,17 @@ class IHyperOpt(ABC): # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. SKDecimal(0.001, 0.1, decimals=3, name="trailing_stop_positive_offset_p1"), - ft_CategoricalDistribution([True, False], name="trailing_only_offset_is_reached"), + Categorical([True, False], name="trailing_only_offset_is_reached"), ] - def max_open_trades_space(self) -> list[DimensionProtocol]: + def max_open_trades_space(self) -> list[Dimension]: """ Create a max open trades space. You may override it in your custom Hyperopt class. """ return [ - ft_IntDistribution(-1, 10, name="max_open_trades"), + Integer(-1, 10, name="max_open_trades"), ] # This is needed for proper unpickling the class attribute timeframe From fb64ac942b216dfd52972adf76ef218c672d9fdf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Apr 2025 20:18:58 +0200 Subject: [PATCH 39/64] chore: raise freqtrade exception so upstream handling is in place --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 0a616927d..653d3df22 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -406,7 +406,7 @@ class HyperOptimizer: ): o_dimensions[original_dim.name] = original_dim else: - raise Exception( + raise OperationalException( f"Unknown search space {original_dim.name} - {original_dim} / \ {type(original_dim)}" ) From 8d0ca7f5c195efdc214646520c03b6266a806746 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 27 Apr 2025 21:26:06 +0300 Subject: [PATCH 40/64] remove backtesting from generate_optimizer --- freqtrade/optimize/hyperopt/hyperopt.py | 9 +++--- .../optimize/hyperopt/hyperopt_optimizer.py | 30 ++++++++++--------- tests/optimize/test_hyperopt.py | 2 +- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 6aeb79057..70a85b30b 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -20,7 +20,6 @@ from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural -from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.hyperopt.hyperopt_logger import logging_mp_handle, logging_mp_setup from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput @@ -149,7 +148,7 @@ class Hyperopt: ) def run_optimizer_parallel( - self, parallel: Parallel, backtesting: Backtesting, asked: list[list] + self, parallel: Parallel, asked: list[list] ) -> list[dict[str, Any]]: """Start optimizer in a parallel way""" @@ -162,7 +161,7 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) return parallel( - delayed(wrap_non_picklable_objects(optimizer_wrapper))(backtesting, v) for v in asked + delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked ) def _set_random_state(self, random_state: int | None) -> int: @@ -289,7 +288,7 @@ class Hyperopt: n_points=1, dimensions=self.hyperopter.o_dimensions ) f_val0 = self.hyperopter.generate_optimizer( - self.hyperopter.backtesting, asked[0].params + asked[0].params ) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) @@ -309,7 +308,7 @@ class Hyperopt: f_val = self.run_optimizer_parallel( parallel, - self.hyperopter.backtesting, + # self.hyperopter.backtesting, [asked1.params for asked1 in asked], ) f_val_loss = [v["loss"] for v in f_val] diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 653d3df22..bd003e3c9 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -267,9 +267,7 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - def generate_optimizer( - self, backtesting: Backtesting, raw_params: dict[str, Any] - ) -> dict[str, Any]: + def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -281,26 +279,30 @@ class HyperOptimizer: # Apply parameters if HyperoptTools.has_space(self.config, "buy"): - self.assign_params(backtesting, params_dict, "buy") + self.assign_params(self.backtesting, params_dict, "buy") if HyperoptTools.has_space(self.config, "sell"): - self.assign_params(backtesting, params_dict, "sell") + self.assign_params(self.backtesting, params_dict, "sell") if HyperoptTools.has_space(self.config, "protection"): - self.assign_params(backtesting, params_dict, "protection") + self.assign_params(self.backtesting, params_dict, "protection") if HyperoptTools.has_space(self.config, "roi"): - backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params_dict) + self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( + params_dict + ) if HyperoptTools.has_space(self.config, "stoploss"): - backtesting.strategy.stoploss = params_dict["stoploss"] + self.backtesting.strategy.stoploss = params_dict["stoploss"] if HyperoptTools.has_space(self.config, "trailing"): d = self.custom_hyperopt.generate_trailing_params(params_dict) - backtesting.strategy.trailing_stop = d["trailing_stop"] - backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] - backtesting.strategy.trailing_stop_positive_offset = d["trailing_stop_positive_offset"] - backtesting.strategy.trailing_only_offset_is_reached = d[ + self.backtesting.strategy.trailing_stop = d["trailing_stop"] + self.backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] + self.backtesting.strategy.trailing_stop_positive_offset = d[ + "trailing_stop_positive_offset" + ] + self.backtesting.strategy.trailing_only_offset_is_reached = d[ "trailing_only_offset_is_reached" ] @@ -319,7 +321,7 @@ class HyperOptimizer: self.config.update({"max_open_trades": updated_max_open_trades}) - backtesting.strategy.max_open_trades = updated_max_open_trades + self.backtesting.strategy.max_open_trades = updated_max_open_trades with self.data_pickle_file.open("rb") as f: processed = load(f, mmap_mode="r") @@ -327,7 +329,7 @@ class HyperOptimizer: # Data is not yet analyzed, rerun populate_indicators. processed = self.advise_and_trim(processed) - bt_results = backtesting.backtest( + bt_results = self.backtesting.backtest( processed=processed, start_date=self.min_date, end_date=self.max_date ) backtest_end_time = datetime.now(timezone.utc) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c1497f2b2..0e95d357b 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -606,7 +606,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( - hyperopt.hyperopter.backtesting, optimizer_param + optimizer_param ) assert generate_optimizer_value == response_expected From 04492e75b2b03decc0a061e78ed988d57c44674a Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sun, 27 Apr 2025 22:02:39 +0300 Subject: [PATCH 41/64] remove # Suppress optuna ExperimentalWarning from skopt with warnings.catch_warnings(): from optuna.exceptions import ExperimentalWarning warnings.filterwarnings("ignore", category=FutureWarning) this should be when importing sampler --- .../optimize/hyperopt/hyperopt_optimizer.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index bd003e3c9..53aa8f88d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -10,8 +10,10 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any +import optuna from joblib import dump, load from joblib.externals import cloudpickle +from optuna.exceptions import ExperimentalWarning from pandas import DataFrame # from memory_profiler import profile @@ -29,26 +31,17 @@ from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats +from freqtrade.optimize.space import ( + DimensionProtocol, + SKDecimal, + ft_CategoricalDistribution, + ft_FloatDistribution, + ft_IntDistribution, +) from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.util.dry_run_wallet import get_dry_run_wallet -# Suppress optuna ExperimentalWarning from skopt -with warnings.catch_warnings(): - from optuna.exceptions import ExperimentalWarning - - warnings.filterwarnings("ignore", category=FutureWarning) - # warnings.filterwarnings("ignore", category=ExperimentalWarning) - import optuna - - from freqtrade.optimize.space import ( - DimensionProtocol, - SKDecimal, - ft_CategoricalDistribution, - ft_FloatDistribution, - ft_IntDistribution, - ) - logger = logging.getLogger(__name__) From c32f8e972a6eaa3f2dd27712bc5cc1dcfb6ce60f Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Mon, 28 Apr 2025 18:50:09 +0300 Subject: [PATCH 42/64] move delayed and wrap_non_picklable_objects from hyperopt to hyperopt_optimizer --- freqtrade/optimize/hyperopt/hyperopt.py | 6 ++---- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 70a85b30b..ed404e923 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Any import rapidjson -from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects +from joblib import Parallel, cpu_count from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState @@ -160,9 +160,7 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) - return parallel( - delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked - ) + return parallel(optimizer_wrapper(v) for v in asked) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 53aa8f88d..b04478841 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any import optuna -from joblib import dump, load +from joblib import dump, load, delayed, wrap_non_picklable_objects from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning from pandas import DataFrame @@ -260,6 +260,8 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] + @delayed + @wrap_non_picklable_objects def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. From 5c47a75f635d352cae5e3a44fb08f58aefdd8505 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Tue, 29 Apr 2025 08:16:44 +0300 Subject: [PATCH 43/64] move back delayed and wrap_non_picklable_objects from hyperopt_optimizer to hyperopt. There are tests failing when using delayed and wrap_non_picklable_objects as decorator. until I'll find a solution to run generate_optimizer standalone for analyze_per_epoch=True --- freqtrade/optimize/hyperopt/hyperopt.py | 6 ++++-- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index ed404e923..70a85b30b 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Any import rapidjson -from joblib import Parallel, cpu_count +from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState @@ -160,7 +160,9 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) - return parallel(optimizer_wrapper(v) for v in asked) + return parallel( + delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked + ) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index b04478841..53aa8f88d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any import optuna -from joblib import dump, load, delayed, wrap_non_picklable_objects +from joblib import dump, load from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning from pandas import DataFrame @@ -260,8 +260,6 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] - @delayed - @wrap_non_picklable_objects def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. From ced1ce340a5bd10f1f81288c5bc8490174ece3e8 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Tue, 29 Apr 2025 17:09:16 +0300 Subject: [PATCH 44/64] fix some formatting issues --- freqtrade/optimize/hyperopt/hyperopt.py | 13 +++---------- tests/optimize/test_hyperopt.py | 4 +--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 70a85b30b..3eecb3128 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -147,9 +147,7 @@ class Hyperopt: self.print_all, ) - def run_optimizer_parallel( - self, parallel: Parallel, asked: list[list] - ) -> list[dict[str, Any]]: + def run_optimizer_parallel(self, parallel: Parallel, asked: list[list]) -> list[dict[str, Any]]: """Start optimizer in a parallel way""" def optimizer_wrapper(*args, **kwargs): @@ -160,9 +158,7 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) - return parallel( - delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked - ) + return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 @@ -287,9 +283,7 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=1, dimensions=self.hyperopter.o_dimensions ) - f_val0 = self.hyperopter.generate_optimizer( - asked[0].params - ) + f_val0 = self.hyperopter.generate_optimizer(asked[0].params) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) @@ -308,7 +302,6 @@ class Hyperopt: f_val = self.run_optimizer_parallel( parallel, - # self.hyperopter.backtesting, [asked1.params for asked1 in asked], ) f_val_loss = [v["loss"] for v in f_val] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 0e95d357b..1396cb0e1 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -605,9 +605,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( - optimizer_param - ) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) assert generate_optimizer_value == response_expected From 0c66180cf313b4d17f1eeb5e23a9e2cac217d42a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 May 2025 10:15:35 +0200 Subject: [PATCH 45/64] chore: add optuna to hyperopt optional dependencies --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index bf40c456e..edc6c8a0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,8 @@ plot = ["plotly>=4.0"] hyperopt = [ "scipy", "scikit-learn", + "optuna > 4.0.0", + "cmaes", "filelock", ] freqai = [ From ecc637173355c11ca80d0453a6f6ab518950154b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 May 2025 10:19:20 +0200 Subject: [PATCH 46/64] test: reduce amount of mocking --- tests/optimize/test_hyperopt.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1396cb0e1..2303bef8e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1211,10 +1211,6 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, } ) hyperopt = Hyperopt(hyperopt_conf) - mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", - return_value={"max_open_trades": -1}, - ) assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) @@ -1222,7 +1218,7 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, hyperopt.start() - assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 1 + assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 3 def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> None: From 1b2d5a357fd5eb4272c9759b078b871f69c8124e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 May 2025 11:57:17 +0200 Subject: [PATCH 47/64] test: change level of test mock --- tests/optimize/test_hyperopt.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 2303bef8e..76825cc03 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,6 +1,6 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime, timedelta -from functools import wraps +from functools import partial, wraps from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock @@ -1236,9 +1236,15 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N } ) hyperopt = Hyperopt(hyperopt_conf) + + def optuna_mock(hyperopt, *args, **kwargs): + a = hyperopt.get_optuna_asked_points(*args, **kwargs) + a[0]._cached_frozen_trial.params["max_open_trades"] = -1 + return a, [True] + mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", - return_value={"max_open_trades": -1}, + "freqtrade.optimize.hyperopt.Hyperopt.get_asked_points", + side_effect=partial(optuna_mock, hyperopt), ) assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) @@ -1256,8 +1262,8 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N hyperopt = Hyperopt(hyperopt_conf) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", - return_value={"max_open_trades": -1}, + "freqtrade.optimize.hyperopt.Hyperopt.get_asked_points", + side_effect=partial(optuna_mock, hyperopt), ) assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) From 149133cc4486af94affb84801c673a0dcb165c6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 1 May 2025 12:03:04 +0200 Subject: [PATCH 48/64] refactor: remove _get_params_dict method --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 53aa8f88d..33b6a9ba1 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -139,18 +139,6 @@ class HyperOptimizer: cloudpickle.register_pickle_by_value(mod) self.hyperopt_pickle_magic(modules.__bases__) - def _get_params_dict( - self, dimensions: list[DimensionProtocol], raw_params: dict[str, Any] - ) -> dict[str, Any]: - # Ensure the number of dimensions match - # the number of parameters in the list. - if len(raw_params) != len(dimensions): - raise ValueError("Mismatch in number of search-space dimensions.") - - # Return a dict where the keys are the names of the dimensions - # and the values are taken from the list of parameters. - return raw_params - def _get_params_details(self, params: dict) -> dict: """ Return the params for each space @@ -268,7 +256,7 @@ class HyperOptimizer: """ HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) backtest_start_time = datetime.now(timezone.utc) - params_dict = self._get_params_dict(self.dimensions, raw_params) + params_dict = raw_params # Apply parameters if HyperoptTools.has_space(self.config, "buy"): From 73c28890d707da1ff34345982944d40bdc018f1b Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Fri, 2 May 2025 21:00:42 +0300 Subject: [PATCH 49/64] move @delayed and @wrap_non_picklable_objects in hyperopt_optimizer.py one test with analyze_per_epoch is failing --- freqtrade/optimize/hyperopt/hyperopt.py | 8 +++++--- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 4 +++- tests/optimize/test_hyperopt.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 3eecb3128..acc1be7f2 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -14,7 +14,8 @@ from pathlib import Path from typing import Any import rapidjson -from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects +from joblib import Parallel, cpu_count +from inspect import unwrap from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState @@ -158,7 +159,8 @@ class Hyperopt: return self.hyperopter.generate_optimizer(*args, **kwargs) - return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) + # return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) + return parallel(optimizer_wrapper(v) for v in asked) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 @@ -283,7 +285,7 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=1, dimensions=self.hyperopter.o_dimensions ) - f_val0 = self.hyperopter.generate_optimizer(asked[0].params) + f_val0 = unwrap(self.hyperopter.generate_optimizer)(asked[0].params) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 33b6a9ba1..76284d330 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any import optuna -from joblib import dump, load +from joblib import dump, load, delayed, wrap_non_picklable_objects from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning from pandas import DataFrame @@ -248,6 +248,8 @@ class HyperOptimizer: # noinspection PyProtectedMember attr.value = params_dict[attr_name] + @delayed + @wrap_non_picklable_objects def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 76825cc03..97bd989bb 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -605,7 +605,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer._obj(hyperopt.hyperopter, raw_params=optimizer_param) assert generate_optimizer_value == response_expected From dd613ac86ccc616eb491aed5f12b11be4ce63e27 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Fri, 2 May 2025 21:07:34 +0300 Subject: [PATCH 50/64] fix formatting --- freqtrade/optimize/hyperopt/hyperopt.py | 2 +- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- tests/optimize/test_hyperopt.py | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index acc1be7f2..a943b734b 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -8,6 +8,7 @@ import gc import logging import random from datetime import datetime +from inspect import unwrap from math import ceil from multiprocessing import Manager from pathlib import Path @@ -15,7 +16,6 @@ from typing import Any import rapidjson from joblib import Parallel, cpu_count -from inspect import unwrap from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 76284d330..f055e71aa 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -11,7 +11,7 @@ from pathlib import Path from typing import Any import optuna -from joblib import dump, load, delayed, wrap_non_picklable_objects +from joblib import delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning from pandas import DataFrame diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 97bd989bb..b49bdc046 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -605,7 +605,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer._obj(hyperopt.hyperopter, raw_params=optimizer_param) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer._obj( + hyperopt.hyperopter, raw_params=optimizer_param + ) assert generate_optimizer_value == response_expected From d9ed7e1fb2a99495fd0031e7a96b3817a7249c7b Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 3 May 2025 09:49:34 +0300 Subject: [PATCH 51/64] remove backtest from assign_params --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index f055e71aa..7056a247f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -238,12 +238,12 @@ class HyperOptimizer: ) def assign_params( - self, backtesting: Backtesting, params_dict: dict[str, Any], category: str + self, params_dict: dict[str, Any], category: str ) -> None: """ Assign hyperoptable parameters """ - for attr_name, attr in backtesting.strategy.enumerate_parameters(category): + for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category): if attr.optimize: # noinspection PyProtectedMember attr.value = params_dict[attr_name] @@ -262,13 +262,13 @@ class HyperOptimizer: # Apply parameters if HyperoptTools.has_space(self.config, "buy"): - self.assign_params(self.backtesting, params_dict, "buy") + self.assign_params(params_dict, "buy") if HyperoptTools.has_space(self.config, "sell"): - self.assign_params(self.backtesting, params_dict, "sell") + self.assign_params(params_dict, "sell") if HyperoptTools.has_space(self.config, "protection"): - self.assign_params(self.backtesting, params_dict, "protection") + self.assign_params(params_dict, "protection") if HyperoptTools.has_space(self.config, "roi"): self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( From 1013c323169680700ca57b26eb179986885ea635 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 May 2025 09:19:50 +0200 Subject: [PATCH 52/64] fix: duplicate generate_optimizer to have a non-delayed alternative --- freqtrade/optimize/hyperopt/hyperopt.py | 2 +- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 7 ++++--- tests/optimize/test_hyperopt.py | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index a943b734b..2ff574036 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -157,7 +157,7 @@ class Hyperopt: log_queue, logging.INFO if self.config["verbosity"] < 1 else logging.DEBUG ) - return self.hyperopter.generate_optimizer(*args, **kwargs) + return self.hyperopter.generate_optimizer_wrapped(*args, **kwargs) # return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) return parallel(optimizer_wrapper(v) for v in asked) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 7056a247f..6607aef90 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -237,9 +237,7 @@ class HyperOptimizer: + self.max_open_trades_space ) - def assign_params( - self, params_dict: dict[str, Any], category: str - ) -> None: + def assign_params(self, params_dict: dict[str, Any], category: str) -> None: """ Assign hyperoptable parameters """ @@ -250,6 +248,9 @@ class HyperOptimizer: @delayed @wrap_non_picklable_objects + def generate_optimizer_wrapped(self, raw_params: dict[str, Any]) -> dict[str, Any]: + return self.generate_optimizer(raw_params) + def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b49bdc046..76825cc03 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -605,9 +605,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) hyperopt.hyperopter.init_spaces() - generate_optimizer_value = hyperopt.hyperopter.generate_optimizer._obj( - hyperopt.hyperopter, raw_params=optimizer_param - ) + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer(optimizer_param) assert generate_optimizer_value == response_expected From 9eea958e17b5f894d94f45b8258e489116a742a5 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 3 May 2025 16:55:41 +0300 Subject: [PATCH 53/64] remove unwrap --- freqtrade/optimize/hyperopt/hyperopt.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 2ff574036..5e31cf84b 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -8,7 +8,6 @@ import gc import logging import random from datetime import datetime -from inspect import unwrap from math import ceil from multiprocessing import Manager from pathlib import Path @@ -285,7 +284,7 @@ class Hyperopt: asked, is_random = self.get_asked_points( n_points=1, dimensions=self.hyperopter.o_dimensions ) - f_val0 = unwrap(self.hyperopter.generate_optimizer)(asked[0].params) + f_val0 = self.hyperopter.generate_optimizer(asked[0].params) self.opt.tell(asked[0], [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) From 6f4ffa07585954b9c7af5656364c95acdd0c09ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 May 2025 19:32:09 +0200 Subject: [PATCH 54/64] chore: minor cleanup --- freqtrade/optimize/hyperopt/hyperopt.py | 1 - freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 5e31cf84b..3db5ec9fe 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -158,7 +158,6 @@ class Hyperopt: return self.hyperopter.generate_optimizer_wrapped(*args, **kwargs) - # return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) return parallel(optimizer_wrapper(v) for v in asked) def _set_random_state(self, random_state: int | None) -> int: diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 6607aef90..8247ccc35 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -16,7 +16,6 @@ from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning from pandas import DataFrame -# from memory_profiler import profile from freqtrade.constants import DATETIME_PRINT_FORMAT, Config from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange @@ -248,10 +247,10 @@ class HyperOptimizer: @delayed @wrap_non_picklable_objects - def generate_optimizer_wrapped(self, raw_params: dict[str, Any]) -> dict[str, Any]: - return self.generate_optimizer(raw_params) + def generate_optimizer_wrapped(self, params_dict: dict[str, Any]) -> dict[str, Any]: + return self.generate_optimizer(params_dict) - def generate_optimizer(self, raw_params: dict[str, Any]) -> dict[str, Any]: + def generate_optimizer(self, params_dict: dict[str, Any]) -> dict[str, Any]: """ Used Optimize function. Called once per epoch to optimize whatever is configured. @@ -259,7 +258,6 @@ class HyperOptimizer: """ HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) backtest_start_time = datetime.now(timezone.utc) - params_dict = raw_params # Apply parameters if HyperoptTools.has_space(self.config, "buy"): From 43bd2a060c3fade33dd229b01a832e8424b945ba Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 7 May 2025 17:20:17 +0300 Subject: [PATCH 55/64] fix optuna userwarning the range is not divisible by `step` --- freqtrade/optimize/hyperopt/hyperopt.py | 1 + freqtrade/optimize/hyperopt_tools.py | 31 +++++++++++++++++++++--- freqtrade/optimize/space/__init__.py | 10 ++------ freqtrade/optimize/space/decimalspace.py | 31 ++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 5e31cf84b..d86252a49 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -331,6 +331,7 @@ class Hyperopt: self.config, self.hyperopter.get_strategy_name(), self.current_best_epoch, + self.hyperopter.o_dimensions, ) HyperoptTools.show_epoch_details( diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ff4747781..f2178d63e 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from typing import Any import numpy as np import rapidjson +from optuna.distributions import BaseDistribution from pandas import isna, json_normalize from freqtrade.constants import FTHYPT_FILEVERSION, Config @@ -14,6 +15,7 @@ from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2 from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs +from freqtrade.optimize.space import SKDecimal, _adjust_discrete_uniform logger = logging.getLogger(__name__) @@ -61,11 +63,26 @@ class HyperoptTools: return None @staticmethod - def export_params(params, strategy_name: str, filename: Path): + def export_params( + params, + strategy_name: str, + filename: Path, + o_dimensions: dict[str, BaseDistribution] | None = None, + ): """ Generate files """ final_params = deepcopy(params["params_not_optimized"]) + if o_dimensions: + for key, val in params["params_details"].items(): + if isinstance(val, dict): + for key1, val1 in val.items(): + if isinstance(o_dimensions.get(key1), SKDecimal): + step = getattr(o_dimensions.get(key1), "step", None) + if step: + params["params_details"][key][key1] = _adjust_discrete_uniform( + val1, step + ) final_params = deep_merge_dicts(params["params_details"], final_params) final_params = { "strategy_name": strategy_name, @@ -73,6 +90,7 @@ class HyperoptTools: "ft_stratparam_v": 1, "export_time": datetime.now(timezone.utc), } + logger.info(f"Dumping parameters to {filename}") with filename.open("w") as f: rapidjson.dump( @@ -93,12 +111,19 @@ class HyperoptTools: return params @staticmethod - def try_export_params(config: Config, strategy_name: str, params: dict): + def try_export_params( + config: Config, + strategy_name: str, + params: dict, + o_dimensions: dict[str, BaseDistribution] | None = None, + ): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get("disableparamexport", False): # Export parameters ... fn = HyperoptTools.get_strategy_filename(config, strategy_name) if fn: - HyperoptTools.export_params(params, strategy_name, fn.with_suffix(".json")) + HyperoptTools.export_params( + params, strategy_name, fn.with_suffix(".json"), o_dimensions + ) else: logger.warning("Strategy not found, not exporting parameter file.") diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index 77552b9f2..a32937864 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,4 +1,4 @@ -from .decimalspace import SKDecimal +from .decimalspace import SKDecimal, _adjust_discrete_uniform from .optunaspaces import ( DimensionProtocol, ft_CategoricalDistribution, @@ -13,10 +13,4 @@ Categorical = ft_CategoricalDistribution Integer = ft_IntDistribution Real = ft_FloatDistribution -__all__ = [ - "Categorical", - "Dimension", - "Integer", - "Real", - "SKDecimal", -] +__all__ = ["Categorical", "Dimension", "Integer", "Real", "SKDecimal", "_adjust_discrete_uniform"] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index c93e49c01..adb90eaf7 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,3 +1,5 @@ +import decimal + from optuna.distributions import FloatDistribution @@ -20,7 +22,32 @@ class SKDecimal(FloatDistribution): self.name = name super().__init__( - low=low, - high=high, + low=_adjust_discrete_uniform(low, self.step), + high=_adjust_discrete_uniform_high(low, high, self.step), step=self.step, ) + + +def _adjust_discrete_uniform_high(low: float, high: float, step: float | None) -> float: + if step: + d_high = decimal.Decimal(str(high)) + d_low = decimal.Decimal(str(low)) + d_step = decimal.Decimal(str(step)) + + d_r = d_high - d_low + + if d_r % d_step != decimal.Decimal("0"): + high = float((d_r // d_step) * d_step + d_low) + + return high + + +def _adjust_discrete_uniform(val: float, step: float | None) -> float: + if step: + d_val = decimal.Decimal(str(val)) + d_step = decimal.Decimal(str(step)) + + if d_val % d_step != decimal.Decimal("0"): + val = float((d_val // d_step) * d_step) + + return val From 5d2f5ec12f002bfc3a82422b24b7e01e46a22e73 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Wed, 7 May 2025 21:57:56 +0300 Subject: [PATCH 56/64] change SKDecimal low/high to be rounded by decimals --- freqtrade/optimize/hyperopt/hyperopt.py | 1 - freqtrade/optimize/hyperopt_tools.py | 18 +------------- freqtrade/optimize/space/__init__.py | 4 +-- freqtrade/optimize/space/decimalspace.py | 31 +++--------------------- 4 files changed, 6 insertions(+), 48 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 131fca753..3db5ec9fe 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -330,7 +330,6 @@ class Hyperopt: self.config, self.hyperopter.get_strategy_name(), self.current_best_epoch, - self.hyperopter.o_dimensions, ) HyperoptTools.show_epoch_details( diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index f2178d63e..aaea6c016 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,7 +7,6 @@ from typing import Any import numpy as np import rapidjson -from optuna.distributions import BaseDistribution from pandas import isna, json_normalize from freqtrade.constants import FTHYPT_FILEVERSION, Config @@ -15,7 +14,6 @@ from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2 from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs -from freqtrade.optimize.space import SKDecimal, _adjust_discrete_uniform logger = logging.getLogger(__name__) @@ -67,22 +65,11 @@ class HyperoptTools: params, strategy_name: str, filename: Path, - o_dimensions: dict[str, BaseDistribution] | None = None, ): """ Generate files """ final_params = deepcopy(params["params_not_optimized"]) - if o_dimensions: - for key, val in params["params_details"].items(): - if isinstance(val, dict): - for key1, val1 in val.items(): - if isinstance(o_dimensions.get(key1), SKDecimal): - step = getattr(o_dimensions.get(key1), "step", None) - if step: - params["params_details"][key][key1] = _adjust_discrete_uniform( - val1, step - ) final_params = deep_merge_dicts(params["params_details"], final_params) final_params = { "strategy_name": strategy_name, @@ -115,15 +102,12 @@ class HyperoptTools: config: Config, strategy_name: str, params: dict, - o_dimensions: dict[str, BaseDistribution] | None = None, ): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get("disableparamexport", False): # Export parameters ... fn = HyperoptTools.get_strategy_filename(config, strategy_name) if fn: - HyperoptTools.export_params( - params, strategy_name, fn.with_suffix(".json"), o_dimensions - ) + HyperoptTools.export_params(params, strategy_name, fn.with_suffix(".json")) else: logger.warning("Strategy not found, not exporting parameter file.") diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index a32937864..4a9855234 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,4 +1,4 @@ -from .decimalspace import SKDecimal, _adjust_discrete_uniform +from .decimalspace import SKDecimal from .optunaspaces import ( DimensionProtocol, ft_CategoricalDistribution, @@ -13,4 +13,4 @@ Categorical = ft_CategoricalDistribution Integer = ft_IntDistribution Real = ft_FloatDistribution -__all__ = ["Categorical", "Dimension", "Integer", "Real", "SKDecimal", "_adjust_discrete_uniform"] +__all__ = ["Categorical", "Dimension", "Integer", "Real", "SKDecimal"] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index adb90eaf7..ef6945e24 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,4 +1,4 @@ -import decimal +from math import log10 from optuna.distributions import FloatDistribution @@ -22,32 +22,7 @@ class SKDecimal(FloatDistribution): self.name = name super().__init__( - low=_adjust_discrete_uniform(low, self.step), - high=_adjust_discrete_uniform_high(low, high, self.step), + low=round(low, int(log10(1 / self.step))) if self.step < 1 else low, + high=round(high, int(log10(1 / self.step))) if self.step < 1 else high, step=self.step, ) - - -def _adjust_discrete_uniform_high(low: float, high: float, step: float | None) -> float: - if step: - d_high = decimal.Decimal(str(high)) - d_low = decimal.Decimal(str(low)) - d_step = decimal.Decimal(str(step)) - - d_r = d_high - d_low - - if d_r % d_step != decimal.Decimal("0"): - high = float((d_r // d_step) * d_step + d_low) - - return high - - -def _adjust_discrete_uniform(val: float, step: float | None) -> float: - if step: - d_val = decimal.Decimal(str(val)) - d_step = decimal.Decimal(str(step)) - - if d_val % d_step != decimal.Decimal("0"): - val = float((d_val // d_step) * d_step) - - return val From 47e1d209dbe3dffcab1a39fdd52bda20560f6c14 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Thu, 8 May 2025 05:49:34 +0300 Subject: [PATCH 57/64] round to decimals low and high --- freqtrade/optimize/space/decimalspace.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index ef6945e24..ff0e0190d 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -1,5 +1,3 @@ -from math import log10 - from optuna.distributions import FloatDistribution @@ -22,7 +20,7 @@ class SKDecimal(FloatDistribution): self.name = name super().__init__( - low=round(low, int(log10(1 / self.step))) if self.step < 1 else low, - high=round(high, int(log10(1 / self.step))) if self.step < 1 else high, + low=round(low, decimals) if decimals else low, + high=round(high, decimals) if decimals else high, step=self.step, ) From f94fd7d5fc28bff9da82fdf60d989110034888d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 May 2025 07:07:22 +0200 Subject: [PATCH 58/64] chore: minor formatting --- freqtrade/optimize/hyperopt_tools.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index aaea6c016..ff4747781 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -61,11 +61,7 @@ class HyperoptTools: return None @staticmethod - def export_params( - params, - strategy_name: str, - filename: Path, - ): + def export_params(params, strategy_name: str, filename: Path): """ Generate files """ @@ -77,7 +73,6 @@ class HyperoptTools: "ft_stratparam_v": 1, "export_time": datetime.now(timezone.utc), } - logger.info(f"Dumping parameters to {filename}") with filename.open("w") as f: rapidjson.dump( @@ -98,11 +93,7 @@ class HyperoptTools: return params @staticmethod - def try_export_params( - config: Config, - strategy_name: str, - params: dict, - ): + def try_export_params(config: Config, strategy_name: str, params: dict): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get("disableparamexport", False): # Export parameters ... fn = HyperoptTools.get_strategy_filename(config, strategy_name) From 8f8da51808a8be356c310a8406d8445a62747761 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 May 2025 19:24:31 +0200 Subject: [PATCH 59/64] feat: round hyperopt results to 13 digits (this removes floating point errors) --- .../optimize/hyperopt/hyperopt_optimizer.py | 41 +++++++++++-------- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 8247ccc35..891bfcbee 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -22,7 +22,7 @@ from freqtrade.data.history import get_timerange from freqtrade.data.metrics import calculate_market_change from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException -from freqtrade.misc import deep_merge_dicts +from freqtrade.misc import deep_merge_dicts, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOptLoss to allow unpickling classes from these modules @@ -145,27 +145,36 @@ class HyperOptimizer: result: dict = {} if HyperoptTools.has_space(self.config, "buy"): - result["buy"] = {p.name: params.get(p.name) for p in self.buy_space} + result["buy"] = round_dict({p.name: params.get(p.name) for p in self.buy_space}, 13) if HyperoptTools.has_space(self.config, "sell"): - result["sell"] = {p.name: params.get(p.name) for p in self.sell_space} + result["sell"] = round_dict({p.name: params.get(p.name) for p in self.sell_space}, 13) if HyperoptTools.has_space(self.config, "protection"): - result["protection"] = {p.name: params.get(p.name) for p in self.protection_space} + result["protection"] = round_dict( + {p.name: params.get(p.name) for p in self.protection_space}, 13 + ) if HyperoptTools.has_space(self.config, "roi"): - result["roi"] = { - str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items() - } + result["roi"] = round_dict( + {str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()}, 13 + ) if HyperoptTools.has_space(self.config, "stoploss"): - result["stoploss"] = {p.name: params.get(p.name) for p in self.stoploss_space} + result["stoploss"] = round_dict( + {p.name: params.get(p.name) for p in self.stoploss_space}, 13 + ) if HyperoptTools.has_space(self.config, "trailing"): - result["trailing"] = self.custom_hyperopt.generate_trailing_params(params) + result["trailing"] = round_dict( + self.custom_hyperopt.generate_trailing_params(params), 13 + ) if HyperoptTools.has_space(self.config, "trades"): - result["max_open_trades"] = { - "max_open_trades": ( - self.backtesting.strategy.max_open_trades - if self.backtesting.strategy.max_open_trades != float("inf") - else -1 - ) - } + result["max_open_trades"] = round_dict( + { + "max_open_trades": ( + self.backtesting.strategy.max_open_trades + if self.backtesting.strategy.max_open_trades != float("inf") + else -1 + ) + }, + 13, + ) return result diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 76825cc03..97d7c0464 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -577,7 +577,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "buy_plusdi": 0.02, "buy_rsi": 35, }, - "roi": {"0": 0.12000000000000001, "20.0": 0.02, "50.0": 0.01, "110.0": 0}, + "roi": {"0": 0.12, "20.0": 0.02, "50.0": 0.01, "110.0": 0}, "protection": { "protection_cooldown_lookback": 20, "protection_enabled": True, From ce7d81325d68594d08797e12109e7a69386e2927 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 May 2025 06:32:43 +0200 Subject: [PATCH 60/64] chore: improved docstring for skdecimal --- freqtrade/optimize/space/decimalspace.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index ff0e0190d..bcf74feb8 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -6,12 +6,19 @@ class SKDecimal(FloatDistribution): self, low: float, high: float, + *, step: float | None = None, decimals: int = 3, name=None, ): """ FloatDistribution with a fixed step size. + Only one of step or decimals can be set. + :param low: lower bound + :param high: upper bound + :param step: step size (e.g. 0.001) + :param decimals: number of decimal places to round to (e.g. 3) + :param name: name of the distribution """ if decimals is not None and step is not None: raise ValueError("You can only set one of decimals or step") From d0d40f4fce96cd330a923bd837739975df7a2f13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 May 2025 06:34:33 +0200 Subject: [PATCH 61/64] test: Improve skdecimal test --- tests/optimize/test_hyperopt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5226c1128..0e3d24511 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1194,6 +1194,14 @@ def test_SKDecimal(): assert not space._contains(1.511) assert not space._contains(1.111222) + with pytest.raises(ValueError): + SKDecimal(1, 2, step=5, decimals=0.2) + + s = SKDecimal(1, 2, step=0.1, decimals=None) + assert s.step == 0.1 + assert s._contains(1.1) + assert not s._contains(1.11) + def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, fee) -> None: # This test is to ensure that unlimited max_open_trades are ignored for the backtesting From a6d3995013dda8c782586de9f4e46c704365fd19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 May 2025 06:34:59 +0200 Subject: [PATCH 62/64] feat: Improved typing, exception if neither step nor decimals is set --- freqtrade/optimize/space/decimalspace.py | 4 +++- tests/optimize/test_hyperopt.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index bcf74feb8..40a8115f9 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -8,7 +8,7 @@ class SKDecimal(FloatDistribution): high: float, *, step: float | None = None, - decimals: int = 3, + decimals: int | None = None, name=None, ): """ @@ -22,6 +22,8 @@ class SKDecimal(FloatDistribution): """ if decimals is not None and step is not None: raise ValueError("You can only set one of decimals or step") + if decimals is None and step is None: + raise ValueError("You must set one of decimals or step") # Convert decimals to step self.step = step or 1 / 10**decimals self.name = name diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 0e3d24511..c22323bdf 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1197,6 +1197,9 @@ def test_SKDecimal(): with pytest.raises(ValueError): SKDecimal(1, 2, step=5, decimals=0.2) + with pytest.raises(ValueError): + SKDecimal(1, 2, step=None, decimals=None) + s = SKDecimal(1, 2, step=0.1, decimals=None) assert s.step == 0.1 assert s._contains(1.1) From 87061bcce838a71407f7f5d3b673c8c2b8e55160 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 May 2025 06:42:37 +0200 Subject: [PATCH 63/64] docs: adopt autosampler example as advanced hyperopt approach --- docs/advanced-hyperopt.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 5b7092f51..1f727398b 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -179,6 +179,36 @@ Some research will be necessary to find additional Samplers (from optunahub) for While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. If you're unsure about this, best use one of the Defaults (`"NSGAIIISampler"` has proven to be the most versatile) without further parameters. +??? Example "Using `AutoSampler` from Optunahub" + + [AutoSampler docs](https://hub.optuna.org/samplers/auto_sampler/) + + Install the necessary dependencies + ``` bash + pip install optunahub cmaes torch scipy + ``` + Implement `generate_estimator()` in your strategy + + ``` python + # ... + from freqtrade.strategy.interface import IStrategy + from typing import List + import optunahub + # ... + + class my_strategy(IStrategy): + class HyperOpt: + def generate_estimator(dimensions: List["Dimension"], **kwargs): + if "random_state" in kwargs.keys(): + return optunahub.load_module("samplers/auto_sampler").AutoSampler(seed=kwargs["random_state"]) + else: + return optunahub.load_module("samplers/auto_sampler").AutoSampler() + + ``` + + Obviously the same approach will work for all other Samplers optuna supports. + + ## Space options For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: From 575c381e65943a6294f4d954da690fda937417d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 May 2025 09:36:08 +0200 Subject: [PATCH 64/64] chore: fix mypy error --- freqtrade/optimize/space/decimalspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index 40a8115f9..dc6dba04d 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -25,7 +25,7 @@ class SKDecimal(FloatDistribution): if decimals is None and step is None: raise ValueError("You must set one of decimals or step") # Convert decimals to step - self.step = step or 1 / 10**decimals + self.step = step or (1 / 10**decimals if decimals else 1) self.name = name super().__init__(