From 384ed3fafba0ff312cbbf819f96ec774c858f3b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 11:49:54 +0100 Subject: [PATCH 01/51] feat: don't limit spaces to builtin spaces --- freqtrade/config_schema/config_schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/config_schema/config_schema.py b/freqtrade/config_schema/config_schema.py index 250632c6a..d8b0b77fb 100644 --- a/freqtrade/config_schema/config_schema.py +++ b/freqtrade/config_schema/config_schema.py @@ -8,7 +8,6 @@ from freqtrade.constants import ( BACKTEST_CACHE_AGE, DRY_RUN_WALLET, EXPORT_OPTIONS, - HYPEROPT_BUILTIN_SPACES, HYPEROPT_LOSS_BUILTIN, MARGIN_MODES, ORDERTIF_POSSIBILITIES, @@ -260,7 +259,7 @@ CONF_SCHEMA = { "includes all spaces except for 'trailing', 'protection', and 'trades'." ), "type": "array", - "items": {"type": "string", "enum": HYPEROPT_BUILTIN_SPACES}, + "items": {"type": "string"}, "default": ["default"], }, "analyze_per_epoch": { From a792744c0d192bcb4ad56468d341e31166cd3c70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 12:05:05 +0100 Subject: [PATCH 02/51] refactor: improve get_indicator space naming --- freqtrade/optimize/hyperopt/hyperopt_auto.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 6fabaaf35..4520f4a22 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -7,6 +7,7 @@ This module implements a convenience auto-hyperopt class, which can be used toge import logging from collections.abc import Callable from contextlib import suppress +from typing import Literal from freqtrade.exceptions import OperationalException @@ -59,7 +60,7 @@ class HyperOptAuto(IHyperOpt): if attr.optimize: yield attr.get_space(attr_name) - def _get_indicator_space(self, category) -> list: + def get_indicator_space(self, category: Literal["buy", "sell", "protection"]) -> list: # TODO: is this necessary, or can we call "generate_space" directly? indicator_space = list(self._generate_indicator_space(category)) if len(indicator_space) > 0: @@ -71,13 +72,13 @@ class HyperOptAuto(IHyperOpt): return [] def buy_indicator_space(self) -> list["Dimension"]: - return self._get_indicator_space("buy") + return self.get_indicator_space("buy") def sell_indicator_space(self) -> list["Dimension"]: - return self._get_indicator_space("sell") + return self.get_indicator_space("sell") def protection_space(self) -> list["Dimension"]: - return self._get_indicator_space("protection") + return self.get_indicator_space("protection") def generate_roi_table(self, params: dict) -> dict[int, float]: return self._get_func("generate_roi_table")(params) From 4cac68d774ff44027032a2f75b2ffaa0ed68b1b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 12:10:11 +0100 Subject: [PATCH 03/51] chore: refactor buy/sell spaces slightly --- .../optimize/hyperopt/hyperopt_optimizer.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index f905c9733..6b8f69d32 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -70,8 +70,7 @@ class HyperOptimizer: """ def __init__(self, config: Config, data_pickle_file: Path) -> None: - self.buy_space: list[DimensionProtocol] = [] - self.sell_space: list[DimensionProtocol] = [] + self.spaces: dict[str, list[DimensionProtocol]] = {} self.protection_space: list[DimensionProtocol] = [] self.roi_space: list[DimensionProtocol] = [] self.stoploss_space: list[DimensionProtocol] = [] @@ -167,10 +166,10 @@ class HyperOptimizer: """ result: dict = {} - if HyperoptTools.has_space(self.config, "buy"): - 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"] = round_dict({p.name: params.get(p.name) for p in self.sell_space}, 13) + for indicator in self.spaces.keys(): + result[indicator] = round_dict( + {p.name: params.get(p.name) for p in self.spaces.get(indicator, [])}, 13 + ) if HyperoptTools.has_space(self.config, "protection"): result["protection"] = round_dict( {p.name: params.get(p.name) for p in self.protection_space}, 13 @@ -234,13 +233,10 @@ class HyperOptimizer: self.backtesting.enable_protections = True self.protection_space = self.custom_hyperopt.protection_space() - if HyperoptTools.has_space(self.config, "buy"): - logger.debug("Hyperopt has 'buy' space") - self.buy_space = self.custom_hyperopt.buy_indicator_space() - - if HyperoptTools.has_space(self.config, "sell"): - logger.debug("Hyperopt has 'sell' space") - self.sell_space = self.custom_hyperopt.sell_indicator_space() + for indicator in ["buy", "sell"]: + if HyperoptTools.has_space(self.config, indicator): + logger.debug(f"Hyperopt has '{indicator}' space") + self.spaces[indicator] = self.custom_hyperopt.get_indicator_space(indicator) if HyperoptTools.has_space(self.config, "roi"): logger.debug("Hyperopt has 'roi' space") @@ -259,8 +255,7 @@ class HyperOptimizer: self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space() self.dimensions = ( - self.buy_space - + self.sell_space + [s for space in self.spaces.values() for s in space] + self.protection_space + self.roi_space + self.stoploss_space From e34e84c5c6b0d88266861cd065ba5c47ef52f297 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 15:38:50 +0100 Subject: [PATCH 04/51] refactor: further refactor of hyperopt spaces --- .../optimize/hyperopt/hyperopt_optimizer.py | 126 ++++++++---------- 1 file changed, 54 insertions(+), 72 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 6b8f69d32..ca067b057 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -71,11 +71,6 @@ class HyperOptimizer: def __init__(self, config: Config, data_pickle_file: Path) -> None: self.spaces: dict[str, 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 = {} @@ -166,37 +161,39 @@ class HyperOptimizer: """ result: dict = {} - for indicator in self.spaces.keys(): - result[indicator] = round_dict( - {p.name: params.get(p.name) for p in self.spaces.get(indicator, [])}, 13 - ) - if HyperoptTools.has_space(self.config, "protection"): - 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"] = 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"] = round_dict( - {p.name: params.get(p.name) for p in self.stoploss_space}, 13 - ) - if HyperoptTools.has_space(self.config, "trailing"): - result["trailing"] = round_dict( - self.custom_hyperopt.generate_trailing_params(params), 13 - ) - if HyperoptTools.has_space(self.config, "trades"): - 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, - ) + for space in self.spaces.keys(): + if space == "protection": + result["protection"] = round_dict( + {p.name: params.get(p.name) for p in self.spaces[space]}, 13 + ) + elif space == "roi": + result["roi"] = round_dict( + {str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items()}, + 13, + ) + elif space == "stoploss": + result["stoploss"] = round_dict( + {p.name: params.get(p.name) for p in self.spaces[space]}, 13 + ) + elif space == "trailing": + result["trailing"] = round_dict( + self.custom_hyperopt.generate_trailing_params(params), 13 + ) + elif space == "trades": + 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, + ) + else: + result[space] = round_dict( + {p.name: params.get(p.name) for p in self.spaces[space]}, 13 + ) return result @@ -225,43 +222,28 @@ class HyperOptimizer: """ Assign the dimensions in the hyperoptimization space. """ - if HyperoptTools.has_space(self.config, "protection"): - # Protections can only be optimized when using the Parameter interface - logger.debug("Hyperopt has 'protection' space") - # Enable Protections if protection space is selected. - self.config["enable_protections"] = True - self.backtesting.enable_protections = True - self.protection_space = self.custom_hyperopt.protection_space() + for space in ["buy", "sell", "protection", "roi", "stoploss", "trailing", "trades"]: + if not HyperoptTools.has_space(self.config, space): + continue + logger.debug(f"Hyperopt has '{space}' space") + if space == "protection": + # Protections can only be optimized when using the Parameter interface + # Enable Protections if protection space is selected. + self.config["enable_protections"] = True + self.backtesting.enable_protections = True + self.spaces[space] = self.custom_hyperopt.protection_space() + elif space == "roi": + self.spaces[space] = self.custom_hyperopt.roi_space() + elif space == "stoploss": + self.spaces[space] = self.custom_hyperopt.stoploss_space() + elif space == "trailing": + self.spaces[space] = self.custom_hyperopt.trailing_space() + elif space == "trades": + self.spaces[space] = self.custom_hyperopt.max_open_trades_space() + else: + self.spaces[space] = self.custom_hyperopt.get_indicator_space(space) - for indicator in ["buy", "sell"]: - if HyperoptTools.has_space(self.config, indicator): - logger.debug(f"Hyperopt has '{indicator}' space") - self.spaces[indicator] = self.custom_hyperopt.get_indicator_space(indicator) - - if HyperoptTools.has_space(self.config, "roi"): - logger.debug("Hyperopt has 'roi' space") - self.roi_space = self.custom_hyperopt.roi_space() - - if HyperoptTools.has_space(self.config, "stoploss"): - logger.debug("Hyperopt has 'stoploss' space") - self.stoploss_space = self.custom_hyperopt.stoploss_space() - - if HyperoptTools.has_space(self.config, "trailing"): - logger.debug("Hyperopt has 'trailing' space") - self.trailing_space = self.custom_hyperopt.trailing_space() - - if HyperoptTools.has_space(self.config, "trades"): - logger.debug("Hyperopt has 'trades' space") - self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space() - - self.dimensions = ( - [s for space in self.spaces.values() for s in space] - + self.protection_space - + self.roi_space - + self.stoploss_space - + self.trailing_space - + self.max_open_trades_space - ) + self.dimensions = [s for space in self.spaces.values() for s in space] def assign_params(self, params_dict: dict[str, Any], category: str) -> None: """ From 886c15a7fb2bfbb2df332b402b480f370e45583b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 15:57:03 +0100 Subject: [PATCH 05/51] test: update test asserting no longer existing metric --- tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index bca0abacf..fc7fa31e1 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -913,7 +913,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: caplog.clear() hyperopt.hyperopter.init_spaces() assert log_has_re(r"The 'protection' space is included into *", caplog) - assert hyperopt.hyperopter.protection_space == [] + assert hyperopt.hyperopter.spaces["protection"] == [] def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: From 97afb4a56ad7b1c078e08b7405a16a9b06051f5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 15:58:50 +0100 Subject: [PATCH 06/51] chore: remove no longer used helper methods --- freqtrade/optimize/hyperopt/hyperopt_auto.py | 9 --------- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 4520f4a22..d1de3945d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -71,15 +71,6 @@ class HyperOptAuto(IHyperOpt): ) return [] - def buy_indicator_space(self) -> list["Dimension"]: - return self.get_indicator_space("buy") - - def sell_indicator_space(self) -> list["Dimension"]: - return self.get_indicator_space("sell") - - def protection_space(self) -> list["Dimension"]: - return self.get_indicator_space("protection") - def generate_roi_table(self, params: dict) -> dict[int, float]: return self._get_func("generate_roi_table")(params) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index ca067b057..bf5fbdd90 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -231,7 +231,7 @@ class HyperOptimizer: # Enable Protections if protection space is selected. self.config["enable_protections"] = True self.backtesting.enable_protections = True - self.spaces[space] = self.custom_hyperopt.protection_space() + self.spaces[space] = self.custom_hyperopt.get_indicator_space(space) elif space == "roi": self.spaces[space] = self.custom_hyperopt.roi_space() elif space == "stoploss": From c6a7b8468410870c44a189fd157b172ca707cfd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 16:00:24 +0100 Subject: [PATCH 07/51] test: update test for new init sequence --- tests/optimize/test_hyperopt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index fc7fa31e1..96a9dbe57 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -906,7 +906,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"): + # The first one to fail raises the exception + with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"): hyperopt.hyperopter.init_spaces() hyperopt.config["hyperopt_ignore_missing_space"] = True From cb7e04bfb02f6c3f99f9b868c0dde25482a5629c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 16:03:01 +0100 Subject: [PATCH 08/51] feat: add explicit scenario testing if any parameter is selected --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index bf5fbdd90..4eda78004 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -244,6 +244,11 @@ class HyperOptimizer: self.spaces[space] = self.custom_hyperopt.get_indicator_space(space) self.dimensions = [s for space in self.spaces.values() for s in space] + if len(self.dimensions) == 0: + raise OperationalException( + "No hyperopt parameters found to optimize. Did you intend to use different spaces?" + ) + self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) def assign_params(self, params_dict: dict[str, Any], category: str) -> None: """ @@ -413,7 +418,6 @@ class HyperOptimizer: 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) if isinstance(o_sampler, str): if o_sampler not in optuna_samplers_dict.keys(): From ffab6c3c506b1627d4e40def1090a7d5d53fa038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 16:03:12 +0100 Subject: [PATCH 09/51] test: Add test for "no hyperopt parameters found" error --- tests/optimize/test_hyperopt.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 96a9dbe57..07399feae 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -917,6 +917,35 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: assert hyperopt.hyperopter.spaces["protection"] == [] +def test_simplified_interface_none_selected(mocker, hyperopt_conf, caplog) -> None: + mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump", MagicMock()) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") + mocker.patch( + "freqtrade.optimize.backtesting.Backtesting.load_bt_data", + MagicMock(return_value=(MagicMock(), None)), + ) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), + ) + + patch_exchange(mocker) + + hyperopt_conf.update( + { + "spaces": [], + } + ) + + hyperopt = Hyperopt(hyperopt_conf) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + # The first one to fail raises the exception + with pytest.raises(OperationalException, match=r"No hyperopt parameters found to optimize\..*"): + hyperopt.hyperopter.init_spaces() + + def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") From c88a92b4f952c8d44be739b95cfd8848e2a25f96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Nov 2025 16:12:28 +0100 Subject: [PATCH 10/51] feat: allow init of random spaces --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 4eda78004..213dbd074 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -222,7 +222,12 @@ class HyperOptimizer: """ Assign the dimensions in the hyperoptimization space. """ - for space in ["buy", "sell", "protection", "roi", "stoploss", "trailing", "trades"]: + spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "trades"] + spaces += [ + s for s in self.config["spaces"] if s not in spaces and s not in ("all", "default") + ] + + for space in spaces: if not HyperoptTools.has_space(self.config, space): continue logger.debug(f"Hyperopt has '{space}' space") From d1224367e5d384180167d2fbf7fa951dc4b20724 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 10:34:03 +0100 Subject: [PATCH 11/51] feat: further enable dynamic hyperopt parameters --- freqtrade/strategy/hyper.py | 48 ++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 6281ab754..3f590d935 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -8,7 +8,7 @@ from collections.abc import Iterator from pathlib import Path from typing import Any -from freqtrade.constants import Config +from freqtrade.constants import HYPEROPT_BUILTIN_SPACES, Config from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -29,9 +29,6 @@ class HyperStrategyMixin: Initialize hyperoptable strategy mixin. """ self.config = config - self.ft_buy_params: list[BaseParameter] = [] - self.ft_sell_params: list[BaseParameter] = [] - self.ft_protection_params: list[BaseParameter] = [] params = self.load_params_from_file() params = params.get("params", {}) @@ -46,15 +43,10 @@ class HyperStrategyMixin: :param category: :return: """ - if category not in ("buy", "sell", "protection", None): - raise OperationalException( - 'Category must be one of: "buy", "sell", "protection", None.' - ) - if category is None: params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params else: - params = getattr(self, f"ft_{category}_params") + params = self._ft_get_param_container(category) for par in params: yield par.name, par @@ -110,20 +102,16 @@ class HyperStrategyMixin: * Parameters defined in parameters objects (buy_params, sell_params, ...) * Parameter defaults """ + spaces = ["buy", "sell", "protection"] + spaces += [ + s for s in self.config["spaces"] if s not in spaces and s not in HYPEROPT_BUILTIN_SPACES + ] - buy_params = deep_merge_dicts( - self._ft_params_from_file.get("buy", {}), getattr(self, "buy_params", {}) - ) - sell_params = deep_merge_dicts( - self._ft_params_from_file.get("sell", {}), getattr(self, "sell_params", {}) - ) - protection_params = deep_merge_dicts( - self._ft_params_from_file.get("protection", {}), getattr(self, "protection_params", {}) - ) - - self._ft_load_params(buy_params, "buy", hyperopt) - self._ft_load_params(sell_params, "sell", hyperopt) - self._ft_load_params(protection_params, "protection", hyperopt) + for space in spaces: + params = deep_merge_dicts( + self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {}) + ) + self._ft_load_params(params, space, hyperopt) def load_params_from_file(self) -> dict: filename_str = getattr(self, "__file__", "") @@ -145,6 +133,18 @@ class HyperStrategyMixin: return {} + def _ft_get_param_container(self, category: str) -> list[BaseParameter]: + """ + Get parameter container for category/space. + Creates the attribute if it does not exist yet. + :param category: category - usually 'buy', 'sell', 'protection',... + :return: list of parameters for category + """ + container_name = f"ft_{category}_params" + if not hasattr(self, container_name): + setattr(self, container_name, []) + return getattr(self, container_name) + def _ft_load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: """ Set optimizable parameter values. @@ -152,7 +152,7 @@ class HyperStrategyMixin: """ if not params: logger.info(f"No params for {space} found, using default values.") - param_container: list[BaseParameter] = getattr(self, f"ft_{space}_params") + param_container: list[BaseParameter] = self._ft_get_param_container(space) for attr_name, attr in detect_parameters(self, space): attr.name = attr_name From 6dc254717713ee08bf7b93f92b422249f615a66c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 10:42:23 +0100 Subject: [PATCH 12/51] feat: Update output for random space names --- freqtrade/optimize/hyperopt_tools.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 4cb9887eb..a112fc556 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -219,21 +219,13 @@ class HyperoptTools: print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) else: - HyperoptTools._params_pretty_print( - params, "buy", "Buy hyperspace params:", non_optimized - ) - HyperoptTools._params_pretty_print( - params, "sell", "Sell hyperspace params:", non_optimized - ) - HyperoptTools._params_pretty_print( - params, "protection", "Protection hyperspace params:", non_optimized - ) - HyperoptTools._params_pretty_print(params, "roi", "ROI table:", non_optimized) - HyperoptTools._params_pretty_print(params, "stoploss", "Stoploss:", non_optimized) - HyperoptTools._params_pretty_print(params, "trailing", "Trailing stop:", non_optimized) - HyperoptTools._params_pretty_print( - params, "max_open_trades", "Max Open Trades:", non_optimized - ) + all_spaces = list(params.keys() | non_optimized.keys()) + spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"] + spaces += [s for s in all_spaces if s not in spaces] + for space in spaces: + HyperoptTools._params_pretty_print( + params, space, f"{space.capitalize()} hyperspace params:", non_optimized + ) @staticmethod def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: From 39c37980d529a91ae48f62ad6535688bd4e464c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 10:45:28 +0100 Subject: [PATCH 13/51] fix: improve resiliance --- freqtrade/strategy/hyper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 3f590d935..15c49c4c3 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -104,7 +104,9 @@ class HyperStrategyMixin: """ spaces = ["buy", "sell", "protection"] spaces += [ - s for s in self.config["spaces"] if s not in spaces and s not in HYPEROPT_BUILTIN_SPACES + s + for s in self.config.get("spaces", []) + if s not in spaces and s not in HYPEROPT_BUILTIN_SPACES ] for space in spaces: From fd0acc074c040c730db0c6a9686b865985622da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 12:07:40 +0100 Subject: [PATCH 14/51] feat: improve parameter detection to detect random named spaces --- freqtrade/strategy/hyper.py | 45 ++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 15c49c4c3..0841bd468 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,6 +4,7 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging +from collections import defaultdict from collections.abc import Iterator from pathlib import Path from typing import Any @@ -54,11 +55,8 @@ class HyperStrategyMixin: @classmethod def detect_all_parameters(cls) -> dict: """Detect all parameters and return them as a list""" - params: dict[str, Any] = { - "buy": list(detect_parameters(cls, "buy")), - "sell": list(detect_parameters(cls, "sell")), - "protection": list(detect_parameters(cls, "protection")), - } + params = detect_all_parameters(cls) + params.update({"count": len(params["buy"] + params["sell"] + params["protection"])}) return params @@ -195,6 +193,7 @@ def detect_parameters( obj: HyperStrategyMixin | type[HyperStrategyMixin], category: str ) -> Iterator[tuple[str, BaseParameter]]: """ + TODO: replace with the below logic completely Detect all parameters for 'category' for "obj" :param obj: Strategy object or class :param category: category - usually `'buy', 'sell', 'protection',... @@ -216,3 +215,39 @@ def detect_parameters( attr_name.startswith(category + "_") and attr.category is None ): yield attr_name, attr + + +def detect_all_parameters( + obj: HyperStrategyMixin | type[HyperStrategyMixin], +) -> dict[str, list[BaseParameter]]: + """ + Detect all hyperoptable parameters for this object. + :param obj: Strategy object or class + """ + auto_categories = ["buy", "sell", "protection"] + result: dict[str, list[BaseParameter]] = defaultdict(list) + for attr_name in dir(obj): + if attr_name.startswith("__"): # Ignore internals + continue + attr = getattr(obj, attr_name) + if not issubclass(attr.__class__, BaseParameter): + continue + category = attr.category + if attr.category is None: + # Category auto detection + for category in auto_categories: + if category == attr.category or ( + attr_name.startswith(category + "_") and attr.category is None + ): + attr.category = category + if attr.category is None or ( + attr_name.startswith(category + "_") + and attr.category is not None + and attr.category != category + ): + raise OperationalException( + f"Inconclusive parameter name {attr_name}, space: {attr.category}." + ) + + result[attr.category].append(attr) + return result From 4b0b306c44a9143ac45deb53aca892165f2e9b4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 12:12:25 +0100 Subject: [PATCH 15/51] fix: hyperoptable should work whenever there's any space detected. --- freqtrade/commands/list_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 8d567d90e..f80bbbc57 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -117,7 +117,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None: if "hyperoptable" in s: objs_to_print[idx].update( { - "hyperoptable": "Yes" if s["hyperoptable"]["count"] > 0 else "No", + "hyperoptable": "Yes" if len(s["hyperoptable"]) > 0 else "No", "buy-Params": str(len(s["hyperoptable"].get("buy", []))), "sell-Params": str(len(s["hyperoptable"].get("sell", []))), } From ac51b41fdfe8cc0496382e6fd19c2c71751d6b50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:01:49 +0100 Subject: [PATCH 16/51] refactor: remove pointless wrapper --- freqtrade/commands/list_commands.py | 3 ++- freqtrade/strategy/hyper.py | 9 --------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index f80bbbc57..2041eb64d 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -140,6 +140,7 @@ def start_list_strategies(args: dict[str, Any]) -> None: """ from freqtrade.configuration import setup_utils_configuration from freqtrade.resolvers import StrategyResolver + from freqtrade.strategy.hyper import detect_all_parameters config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -153,7 +154,7 @@ def start_list_strategies(args: dict[str, Any]) -> None: strategy_objs = sorted(strategy_objs, key=lambda x: x["name"]) for obj in strategy_objs: if obj["class"]: - obj["hyperoptable"] = obj["class"].detect_all_parameters() + obj["hyperoptable"] = detect_all_parameters(obj["class"]) else: obj["hyperoptable"] = {"count": 0} diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 0841bd468..4ae2a4be4 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -52,15 +52,6 @@ class HyperStrategyMixin: for par in params: yield par.name, par - @classmethod - def detect_all_parameters(cls) -> dict: - """Detect all parameters and return them as a list""" - params = detect_all_parameters(cls) - - params.update({"count": len(params["buy"] + params["sell"] + params["protection"])}) - - return params - def ft_load_params_from_file(self) -> None: """ Load Parameters from parameter file From 6f48b8229746ef082dd6eb76bdc73ba8ccce27d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:02:08 +0100 Subject: [PATCH 17/51] test: slight update to test ... --- tests/strategy/test_interface.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index b3ee0c398..a7ede0204 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -16,7 +16,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import detect_parameters +from freqtrade.strategy.hyper import detect_all_parameters, detect_parameters from freqtrade.strategy.parameters import ( IntParameter, ) @@ -940,15 +940,14 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 - all_params = strategy.detect_all_parameters() + # all_params = strategy.detect_all_parameters() + all_params = detect_all_parameters(strategy.__class__) assert isinstance(all_params, dict) # Only one buy param at class level assert len(all_params["buy"]) == 1 # Running detect params at instance level reveals both parameters. assert len(list(detect_parameters(strategy, "buy"))) == 2 assert len(all_params["sell"]) == 2 - # Number of Hyperoptable parameters - assert all_params["count"] == 5 strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space="buy") From 3da6006a44242283cfa9285a2730f7d702a731c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:21:12 +0100 Subject: [PATCH 18/51] feat: improve hyperopt detection logic --- freqtrade/strategy/hyper.py | 57 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 4ae2a4be4..5c54b0f71 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -19,6 +19,11 @@ from freqtrade.strategy.parameters import BaseParameter logger = logging.getLogger(__name__) +# Type aliases +SpaceParams = dict[str, BaseParameter] +AllSpaceParams = dict[str, SpaceParams] + + class HyperStrategyMixin: """ A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell @@ -91,18 +96,13 @@ class HyperStrategyMixin: * Parameters defined in parameters objects (buy_params, sell_params, ...) * Parameter defaults """ - spaces = ["buy", "sell", "protection"] - spaces += [ - s - for s in self.config.get("spaces", []) - if s not in spaces and s not in HYPEROPT_BUILTIN_SPACES - ] + params = detect_all_parameters(self) - for space in spaces: - params = deep_merge_dicts( + for space in params.keys(): + params_values = deep_merge_dicts( self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {}) ) - self._ft_load_params(params, space, hyperopt) + self._ft_load_params(params[space], params_values, space, hyperopt) def load_params_from_file(self) -> dict: filename_str = getattr(self, "__file__", "") @@ -136,34 +136,35 @@ class HyperStrategyMixin: setattr(self, container_name, []) return getattr(self, container_name) - def _ft_load_params(self, params: dict, space: str, hyperopt: bool = False) -> None: + def _ft_load_params( + self, params: SpaceParams, param_values: dict, space: str, hyperopt: bool = False + ) -> None: """ Set optimizable parameter values. :param params: Dictionary with new parameter values. """ - if not params: + if not param_values: logger.info(f"No params for {space} found, using default values.") param_container: list[BaseParameter] = self._ft_get_param_container(space) - for attr_name, attr in detect_parameters(self, space): - attr.name = attr_name - attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space) - if not attr.category: - attr.category = space + for param_name, param in params.items(): + param.in_space = hyperopt and HyperoptTools.has_space(self.config, space) + if not param.category: + param.category = space - param_container.append(attr) + param_container.append(param) - if params and attr_name in params: - if attr.load: - attr.value = params[attr_name] - logger.info(f"Strategy Parameter: {attr_name} = {attr.value}") + if param_values and param_name in param_values: + if param.load: + param.value = param_values[param_name] + logger.info(f"Strategy Parameter: {param_name} = {param.value}") else: logger.warning( - f'Parameter "{attr_name}" exists, but is disabled. ' - f'Default value "{attr.value}" used.' + f'Parameter "{param_name}" exists, but is disabled. ' + f'Default value "{param.value}" used.' ) else: - logger.info(f"Strategy Parameter(default): {attr_name} = {attr.value}") + logger.info(f"Strategy Parameter(default): {param_name} = {param.value}") def get_no_optimize_params(self) -> dict[str, dict]: """ @@ -210,13 +211,13 @@ def detect_parameters( def detect_all_parameters( obj: HyperStrategyMixin | type[HyperStrategyMixin], -) -> dict[str, list[BaseParameter]]: +) -> AllSpaceParams: """ Detect all hyperoptable parameters for this object. :param obj: Strategy object or class """ auto_categories = ["buy", "sell", "protection"] - result: dict[str, list[BaseParameter]] = defaultdict(list) + result: AllSpaceParams = defaultdict(dict) for attr_name in dir(obj): if attr_name.startswith("__"): # Ignore internals continue @@ -239,6 +240,6 @@ def detect_all_parameters( raise OperationalException( f"Inconclusive parameter name {attr_name}, space: {attr.category}." ) - - result[attr.category].append(attr) + attr.name = attr_name + result[attr.category][attr_name] = attr return result From 72c87b7cbd958e55319e63cea3844ca69e456dd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:21:44 +0100 Subject: [PATCH 19/51] chore: remove no longer used function --- freqtrade/strategy/hyper.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 5c54b0f71..23a2f7c43 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -181,34 +181,6 @@ class HyperStrategyMixin: return params -def detect_parameters( - obj: HyperStrategyMixin | type[HyperStrategyMixin], category: str -) -> Iterator[tuple[str, BaseParameter]]: - """ - TODO: replace with the below logic completely - Detect all parameters for 'category' for "obj" - :param obj: Strategy object or class - :param category: category - usually `'buy', 'sell', 'protection',... - """ - for attr_name in dir(obj): - if not attr_name.startswith("__"): # Ignore internals, not strictly necessary. - attr = getattr(obj, attr_name) - if issubclass(attr.__class__, BaseParameter): - if ( - attr_name.startswith(category + "_") - and attr.category is not None - and attr.category != category - ): - raise OperationalException( - f"Inconclusive parameter name {attr_name}, category: {attr.category}." - ) - - if category == attr.category or ( - attr_name.startswith(category + "_") and attr.category is None - ): - yield attr_name, attr - - def detect_all_parameters( obj: HyperStrategyMixin | type[HyperStrategyMixin], ) -> AllSpaceParams: From 4cabbe4d524075fca5071f509556b9779a5f18dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:30:40 +0100 Subject: [PATCH 20/51] test: remove detect_parameters from tests --- tests/strategy/test_interface.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a7ede0204..41599f513 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -16,7 +16,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver -from freqtrade.strategy.hyper import detect_all_parameters, detect_parameters +from freqtrade.strategy.hyper import detect_all_parameters from freqtrade.strategy.parameters import ( IntParameter, ) @@ -946,13 +946,14 @@ def test_auto_hyperopt_interface(default_conf): # Only one buy param at class level assert len(all_params["buy"]) == 1 # Running detect params at instance level reveals both parameters. - assert len(list(detect_parameters(strategy, "buy"))) == 2 - assert len(all_params["sell"]) == 2 + params_inst = detect_all_parameters(strategy) + assert len(params_inst["buy"]) == 2 + assert len(params_inst["sell"]) == 2 strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space="buy") with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): - [x for x in detect_parameters(strategy, "sell")] + detect_all_parameters(strategy.__class__) def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): From 231a1457164a9527cd05aa0c97af373145ba7fa8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:36:17 +0100 Subject: [PATCH 21/51] refactor: improve parameter storage --- freqtrade/strategy/hyper.py | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 23a2f7c43..91e4fb930 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -35,6 +35,7 @@ class HyperStrategyMixin: Initialize hyperoptable strategy mixin. """ self.config = config + self.__ft_hyper_params: AllSpaceParams = {} params = self.load_params_from_file() params = params.get("params", {}) @@ -49,13 +50,9 @@ class HyperStrategyMixin: :param category: :return: """ - if category is None: - params = self.ft_buy_params + self.ft_sell_params + self.ft_protection_params - else: - params = self._ft_get_param_container(category) - - for par in params: - yield par.name, par + for category in [c for c in self.__ft_hyper_params if category is None or c == category]: + for par in self.__ft_hyper_params[category].values(): + yield par.name, par def ft_load_params_from_file(self) -> None: """ @@ -96,13 +93,13 @@ class HyperStrategyMixin: * Parameters defined in parameters objects (buy_params, sell_params, ...) * Parameter defaults """ - params = detect_all_parameters(self) + self.__ft_hyper_params = detect_all_parameters(self) - for space in params.keys(): + for space in self.__ft_hyper_params.keys(): params_values = deep_merge_dicts( self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {}) ) - self._ft_load_params(params[space], params_values, space, hyperopt) + self._ft_load_params(self.__ft_hyper_params[space], params_values, space, hyperopt) def load_params_from_file(self) -> dict: filename_str = getattr(self, "__file__", "") @@ -124,18 +121,6 @@ class HyperStrategyMixin: return {} - def _ft_get_param_container(self, category: str) -> list[BaseParameter]: - """ - Get parameter container for category/space. - Creates the attribute if it does not exist yet. - :param category: category - usually 'buy', 'sell', 'protection',... - :return: list of parameters for category - """ - container_name = f"ft_{category}_params" - if not hasattr(self, container_name): - setattr(self, container_name, []) - return getattr(self, container_name) - def _ft_load_params( self, params: SpaceParams, param_values: dict, space: str, hyperopt: bool = False ) -> None: @@ -145,15 +130,12 @@ class HyperStrategyMixin: """ if not param_values: logger.info(f"No params for {space} found, using default values.") - param_container: list[BaseParameter] = self._ft_get_param_container(space) for param_name, param in params.items(): param.in_space = hyperopt and HyperoptTools.has_space(self.config, space) if not param.category: param.category = space - param_container.append(param) - if param_values and param_name in param_values: if param.load: param.value = param_values[param_name] From 465af62c1692be572551e5a79e7260e09992f163 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 13:57:45 +0100 Subject: [PATCH 22/51] feat: further updates --- freqtrade/strategy/hyper.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 91e4fb930..ba868a3b0 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -35,7 +35,7 @@ class HyperStrategyMixin: Initialize hyperoptable strategy mixin. """ self.config = config - self.__ft_hyper_params: AllSpaceParams = {} + self._ft_hyper_params: AllSpaceParams = {} params = self.load_params_from_file() params = params.get("params", {}) @@ -50,8 +50,8 @@ class HyperStrategyMixin: :param category: :return: """ - for category in [c for c in self.__ft_hyper_params if category is None or c == category]: - for par in self.__ft_hyper_params[category].values(): + for category in [c for c in self._ft_hyper_params if category is None or c == category]: + for par in self._ft_hyper_params[category].values(): yield par.name, par def ft_load_params_from_file(self) -> None: @@ -93,13 +93,13 @@ class HyperStrategyMixin: * Parameters defined in parameters objects (buy_params, sell_params, ...) * Parameter defaults """ - self.__ft_hyper_params = detect_all_parameters(self) + self._ft_hyper_params = detect_all_parameters(self) - for space in self.__ft_hyper_params.keys(): + for space in self._ft_hyper_params.keys(): params_values = deep_merge_dicts( self._ft_params_from_file.get(space, {}), getattr(self, f"{space}_params", {}) ) - self._ft_load_params(self.__ft_hyper_params[space], params_values, space, hyperopt) + self._ft_load_params(self._ft_hyper_params[space], params_values, space, hyperopt) def load_params_from_file(self) -> dict: filename_str = getattr(self, "__file__", "") @@ -152,11 +152,7 @@ class HyperStrategyMixin: """ Returns list of Parameters that are not part of the current optimize job """ - params: dict[str, dict] = { - "buy": {}, - "sell": {}, - "protection": {}, - } + params: dict[str, dict] = defaultdict(dict) for name, p in self.enumerate_parameters(): if p.category and (not p.optimize or not p.in_space): params[p.category][name] = p.value From 2ba91725269b2fe73d90355cd67df72344cef271 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 14:30:08 +0100 Subject: [PATCH 23/51] feat: Assign all matching parameters --- .../optimize/hyperopt/hyperopt_optimizer.py | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 213dbd074..390b535d4 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -255,15 +255,6 @@ class HyperOptimizer: ) self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) - def assign_params(self, params_dict: dict[str, Any], category: str) -> None: - """ - Assign hyperoptable parameters - """ - for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category): - if attr.optimize: - # noinspection PyProtectedMember - attr.value = params_dict[attr_name] - @delayed @wrap_non_picklable_objects def generate_optimizer_wrapped(self, params_dict: dict[str, Any]) -> dict[str, Any]: @@ -279,15 +270,9 @@ class HyperOptimizer: HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) backtest_start_time = datetime.now(UTC) - # Apply parameters - if HyperoptTools.has_space(self.config, "buy"): - self.assign_params(params_dict, "buy") - - if HyperoptTools.has_space(self.config, "sell"): - self.assign_params(params_dict, "sell") - - if HyperoptTools.has_space(self.config, "protection"): - self.assign_params(params_dict, "protection") + for attr_name, attr in self.backtesting.strategy.enumerate_parameters(): + if attr.in_space and attr.optimize: + attr.value = params_dict[attr_name] if HyperoptTools.has_space(self.config, "roi"): self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( From d83f222a13bc2644a236640a3fe63dcbd8bb1505 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Nov 2025 16:29:49 +0100 Subject: [PATCH 24/51] chore: remove unused imports --- freqtrade/strategy/hyper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index ba868a3b0..5bf1c49a2 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -7,9 +7,8 @@ import logging from collections import defaultdict from collections.abc import Iterator from pathlib import Path -from typing import Any -from freqtrade.constants import HYPEROPT_BUILTIN_SPACES, Config +from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.hyperopt_tools import HyperoptTools @@ -165,6 +164,7 @@ def detect_all_parameters( """ Detect all hyperoptable parameters for this object. :param obj: Strategy object or class + :return: Dictionary of detected parameters by space """ auto_categories = ["buy", "sell", "protection"] result: AllSpaceParams = defaultdict(dict) From 8269333d9cbcf63006b6a8cd898f4687946ef5ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Nov 2025 19:59:28 +0100 Subject: [PATCH 25/51] feat: improve output wording --- freqtrade/optimize/hyperopt_tools.py | 3 ++- tests/commands/test_commands.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index a112fc556..b2efec1aa 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -223,8 +223,9 @@ class HyperoptTools: spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"] spaces += [s for s in all_spaces if s not in spaces] for space in spaces: + name = space.capitalize() if space != "roi" else space.upper() HyperoptTools._params_pretty_print( - params, space, f"{space.capitalize()} hyperspace params:", non_optimized + params, space, f"{name} parameters:", non_optimized ) @staticmethod diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 5231d45a4..036352acf 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1319,10 +1319,10 @@ def test_hyperopt_list(mocker, capsys, caplog, tmp_path): " 2/12", " 10/12", "Best result:", - "Buy hyperspace params", - "Sell hyperspace params", - "ROI table", - "Stoploss", + "Buy parameters", + "Sell parameters", + "ROI parameters", + "Stoploss parameters", ] ) assert all( From 367b9fa7f6d7c46de2a8541a30c4b167f32f92de Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Nov 2025 20:02:38 +0100 Subject: [PATCH 26/51] test: fix failing test --- tests/optimize/test_hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 07399feae..805fd3c2b 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -501,7 +501,7 @@ def test_populate_indicators(hyperopt, testdatadir) -> None: def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt_conf.update( { - "spaces": "all", + "spaces": ["all"], "hyperopt_min_trades": 1, } ) @@ -616,7 +616,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "max_open_trades": {"max_open_trades": 3}, }, "params_dict": optimizer_param, - "params_not_optimized": {"buy": {}, "protection": {}, "sell": {}}, + "params_not_optimized": {}, "results_metrics": ANY, "total_profit": 3.1e-08, } From 74a18bdb11157345146333478fecf3e1dfcb99e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Nov 2025 20:17:56 +0100 Subject: [PATCH 27/51] chore: improve output wording/formatting --- freqtrade/optimize/hyperopt_tools.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2efec1aa..4dcc93044 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -9,7 +9,7 @@ import numpy as np import rapidjson from pandas import isna, json_normalize -from freqtrade.constants import FTHYPT_FILEVERSION, Config +from freqtrade.constants import FTHYPT_FILEVERSION, HYPEROPT_BUILTIN_SPACES, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2 @@ -223,7 +223,14 @@ class HyperoptTools: spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"] spaces += [s for s in all_spaces if s not in spaces] for space in spaces: - name = space.capitalize() if space != "roi" else space.upper() + lookup = { + "roi": "ROI", + "trailing": "Trailing stop", + } + name = lookup.get( + space, space.capitalize() if space in HYPEROPT_BUILTIN_SPACES else space + ) + HyperoptTools._params_pretty_print( params, space, f"{name} parameters:", non_optimized ) From ab28e43050fea9c4a5955f4bfb16c749e6a6eed5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Nov 2025 20:18:03 +0100 Subject: [PATCH 28/51] test: update tests to new behavior --- tests/optimize/test_hyperopt_tools.py | 4 ++-- tests/strategy/test_interface.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 2351acbba..1b15ba6b7 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -296,14 +296,14 @@ def test_show_epoch_details(capsys): HyperoptTools.show_epoch_details(test_result, 5, False, no_header=True) captured = capsys.readouterr() - assert "# Trailing stop:" in captured.out + assert "# Trailing stop parameters:" in captured.out # re.match(r"Pairs for .*", captured.out) assert re.search(r"^\s+trailing_stop = True$", captured.out, re.MULTILINE) assert re.search(r"^\s+trailing_stop_positive = 0.02$", captured.out, re.MULTILINE) assert re.search(r"^\s+trailing_stop_positive_offset = 0.04$", captured.out, re.MULTILINE) assert re.search(r"^\s+trailing_only_offset_is_reached = True$", captured.out, re.MULTILINE) - assert "# ROI table:" in captured.out + assert "# ROI parameters:" in captured.out assert re.search(r"^\s+minimal_roi = \{$", captured.out, re.MULTILINE) assert re.search(r"^\s+\"90\"\:\s0.14,\s*$", captured.out, re.MULTILINE) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 41599f513..a8e32ca54 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -928,8 +928,7 @@ def test_auto_hyperopt_interface(default_conf): PairLocks.timeframe = default_conf["timeframe"] strategy = StrategyResolver.load_strategy(default_conf) strategy.ft_bot_start() - with pytest.raises(OperationalException): - next(strategy.enumerate_parameters("deadBeef")) + assert list(strategy.enumerate_parameters("deadBeef")) == [] assert strategy.buy_rsi.value == strategy.buy_params["buy_rsi"] # PlusDI is NOT in the buy-params, so default should be used From c091426c442974d80e32ba1066fe57fc2871ca1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Nov 2025 06:54:42 +0100 Subject: [PATCH 29/51] feat: improve Auto-space detection logic --- freqtrade/strategy/hyper.py | 26 ++++++++++++++------------ tests/strategy/test_interface.py | 9 ++++++++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 5bf1c49a2..847aac102 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -174,21 +174,23 @@ def detect_all_parameters( attr = getattr(obj, attr_name) if not issubclass(attr.__class__, BaseParameter): continue - category = attr.category - if attr.category is None: - # Category auto detection - for category in auto_categories: - if category == attr.category or ( - attr_name.startswith(category + "_") and attr.category is None - ): - attr.category = category - if attr.category is None or ( - attr_name.startswith(category + "_") + auto_category: str | None = None + # Category auto detection + for category in auto_categories: + if attr_name.startswith(category + "_"): + auto_category = category + break + if auto_category is None and attr.category is None: + raise OperationalException(f"Cannot determine parameter space for {attr_name}.") + if auto_category is not None and attr.category is None: + attr.category = auto_category + if ( + auto_category is not None and attr.category is not None - and attr.category != category + and auto_category != attr.category ): raise OperationalException( - f"Inconclusive parameter name {attr_name}, space: {attr.category}." + f"Conflicting parameter space for {attr_name}: {auto_category} vs {attr.category}." ) attr.name = attr_name result[attr.category][attr_name] = attr diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a8e32ca54..324617869 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -951,7 +951,14 @@ def test_auto_hyperopt_interface(default_conf): strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space="buy") - with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"): + with pytest.raises(OperationalException, match=r"Conflicting parameter space.*"): + detect_all_parameters(strategy.__class__) + + strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5) + + with pytest.raises( + OperationalException, match=r"Cannot determine parameter space for exit22_rsi\." + ): detect_all_parameters(strategy.__class__) From c748ac2aa22fdacfdf57390043d67f7f15eae3ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Nov 2025 07:09:13 +0100 Subject: [PATCH 30/51] chore: be more precise in type --- freqtrade/optimize/hyperopt/hyperopt_auto.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index d1de3945d..3120971d6 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -60,7 +60,11 @@ class HyperOptAuto(IHyperOpt): if attr.optimize: yield attr.get_space(attr_name) - def get_indicator_space(self, category: Literal["buy", "sell", "protection"]) -> list: + def get_indicator_space(self, category: Literal["buy", "sell", "protection"] | str) -> list: + """ + Get indicator space for a given space. + :param category: parameter space to get. + """ # TODO: is this necessary, or can we call "generate_space" directly? indicator_space = list(self._generate_indicator_space(category)) if len(indicator_space) > 0: From 007ab1b796462586882693134345550e79018a73 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Nov 2025 07:14:01 +0100 Subject: [PATCH 31/51] test: some cleanup --- tests/optimize/test_hyperopt.py | 1 - tests/strategy/test_interface.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 805fd3c2b..086df46ac 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -941,7 +941,6 @@ def test_simplified_interface_none_selected(mocker, hyperopt_conf, caplog) -> No hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - # The first one to fail raises the exception with pytest.raises(OperationalException, match=r"No hyperopt parameters found to optimize\..*"): hyperopt.hyperopter.init_spaces() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 324617869..bf422d0ec 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -939,7 +939,6 @@ def test_auto_hyperopt_interface(default_conf): # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 - # all_params = strategy.detect_all_parameters() all_params = detect_all_parameters(strategy.__class__) assert isinstance(all_params, dict) # Only one buy param at class level From e22bf5c681f06ae102c246e9b03ddb775fb13fa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Nov 2025 07:16:57 +0100 Subject: [PATCH 32/51] chore: non-loaded strategies shouldn't show as hyperoptable --- freqtrade/commands/list_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 2041eb64d..4c066053a 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -156,7 +156,7 @@ def start_list_strategies(args: dict[str, Any]) -> None: if obj["class"]: obj["hyperoptable"] = detect_all_parameters(obj["class"]) else: - obj["hyperoptable"] = {"count": 0} + obj["hyperoptable"] = {} if args["print_one_column"]: print("\n".join([s["name"] for s in strategy_objs])) From d7e4965cdebca04c7254e0c658523901bb386c63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Nov 2025 07:21:43 +0100 Subject: [PATCH 33/51] feat: improve list-strategies command output --- freqtrade/commands/list_commands.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 4c066053a..8bd0930f1 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -101,7 +101,7 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None: names = [s["name"] for s in objs] objs_to_print: list[dict[str, Text | str]] = [ { - "name": Text(s["name"] if s["name"] else "--"), + "Strategy name": Text(s["name"] if s["name"] else "--"), "location": s["location_rel"], "status": ( Text("LOAD FAILED", style="bold red") @@ -115,11 +115,18 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None: ] for idx, s in enumerate(objs): if "hyperoptable" in s: + custom_params = [ + f"{space}: {len(params)}" + for space, params in s["hyperoptable"].items() + if space not in ["buy", "sell", "protection"] + ] objs_to_print[idx].update( { "hyperoptable": "Yes" if len(s["hyperoptable"]) > 0 else "No", "buy-Params": str(len(s["hyperoptable"].get("buy", []))), "sell-Params": str(len(s["hyperoptable"].get("sell", []))), + "protection-Params": str(len(s["hyperoptable"].get("protection", []))), + "custom-Params": ", ".join(custom_params) if custom_params else "", } ) table = Table() From cfd4926f47b3bdcf8371de343abb19d4d18afeef Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:03:03 +0100 Subject: [PATCH 34/51] chore: fix odd indentation error --- freqtrade/optimize/hyperopt/hyperopt_auto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 3120971d6..30b649ccd 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -38,8 +38,8 @@ def _format_exception_message(space: str, ignore_missing_space: bool) -> None: class HyperOptAuto(IHyperOpt): """ This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. - Most of the time Strategy.HyperOpt class would only implement indicator_space and - sell_indicator_space methods, but other hyperopt methods can be overridden as well. + Most of the time Strategy.HyperOpt class would only implement indicator_space and + sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ def _get_func(self, name) -> Callable: From 1a2f261ee22d82f226822b35315fb20ce024dcee Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:12:56 +0100 Subject: [PATCH 35/51] feat: support "--spaces all" with custom spaces --- freqtrade/optimize/hyperopt/hyperopt_auto.py | 7 +++++++ freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 30b649ccd..603d0b139 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -42,6 +42,13 @@ class HyperOptAuto(IHyperOpt): sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ + def get_available_spaces(self) -> list[str]: + """ + Get list of available spaces defined in strategy. + :return: list of available spaces. + """ + return list(self.strategy._ft_hyper_params) + def _get_func(self, name) -> Callable: """ Return a function defined in Strategy.HyperOpt class, or one defined in super() class. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 390b535d4..167559f9d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -223,9 +223,7 @@ class HyperOptimizer: Assign the dimensions in the hyperoptimization space. """ spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "trades"] - spaces += [ - s for s in self.config["spaces"] if s not in spaces and s not in ("all", "default") - ] + spaces += [s for s in self.custom_hyperopt.get_available_spaces() if s not in spaces] for space in spaces: if not HyperoptTools.has_space(self.config, space): From d1b553cecc1b6b212569991e7549dde97b813951 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:14:28 +0100 Subject: [PATCH 36/51] test: add custom parameter to test strategy --- tests/optimize/test_hyperopt.py | 4 ++++ tests/strategy/strats/hyperoptable_strategy.py | 1 + 2 files changed, 5 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 086df46ac..8ea203bb4 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -569,6 +569,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "buy_rsi": 35, "sell_minusdi": 0.02, "sell_rsi": 75, + "exitaaa": 7, "protection_cooldown_lookback": 20, "protection_enabled": True, "roi_t1": 60.0, @@ -597,6 +598,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "buy_plusdi": 0.02, "buy_rsi": 35, }, + "exit": { + "exitaaa": 7, + }, "roi": {"0": 0.12, "20.0": 0.02, "50.0": 0.01, "110.0": 0}, "protection": { "protection_cooldown_lookback": 20, diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index c5b23a52a..32d1e63bf 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -35,6 +35,7 @@ class HyperoptableStrategy(StrategyTestV3): sell_minusdi = DecimalParameter( low=0, high=1, default=0.5001, decimals=3, space="sell", load=False ) + exitaaa = IntParameter(low=0, high=10, default=5, space="exit") protection_enabled = BooleanParameter(default=True) protection_cooldown_lookback = IntParameter([0, 50], default=30) From 4a8f487b687e143b020b907e69afa837492a0960 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:38:55 +0100 Subject: [PATCH 37/51] chore: update json schema --- build_helpers/schema.json | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index fd45e1cde..d621737fc 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -301,18 +301,7 @@ "description": "Hyperopt parameter spaces to optimize. Default is the default set andincludes all spaces except for 'trailing', 'protection', and 'trades'.", "type": "array", "items": { - "type": "string", - "enum": [ - "all", - "buy", - "sell", - "roi", - "stoploss", - "trailing", - "protection", - "trades", - "default" - ] + "type": "string" }, "default": [ "default" From 91866c1165b15166f41575ca597f82e5dee801b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:46:53 +0100 Subject: [PATCH 38/51] refactor: move lookup dictionary outside of loop --- freqtrade/optimize/hyperopt_tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 4dcc93044..d343f6ee5 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -222,11 +222,11 @@ class HyperoptTools: all_spaces = list(params.keys() | non_optimized.keys()) spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"] spaces += [s for s in all_spaces if s not in spaces] + lookup = { + "roi": "ROI", + "trailing": "Trailing stop", + } for space in spaces: - lookup = { - "roi": "ROI", - "trailing": "Trailing stop", - } name = lookup.get( space, space.capitalize() if space in HYPEROPT_BUILTIN_SPACES else space ) From f214942ff1b20b65550afc703bdf857f8a229198 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:49:32 +0100 Subject: [PATCH 39/51] fix: improve error message for missing hyperopt parameters --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 167559f9d..3d515b27f 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -249,7 +249,10 @@ class HyperOptimizer: self.dimensions = [s for space in self.spaces.values() for s in space] if len(self.dimensions) == 0: raise OperationalException( - "No hyperopt parameters found to optimize. Did you intend to use different spaces?" + "No hyperopt parameters found to optimize." + f"Available spaces: {', '.join(spaces)}. " + "Check your strategy's parameter definitions or verify the configured spaces " + "in your command." ) self.o_dimensions = self.convert_dimensions_to_optuna_space(self.dimensions) From 68dea691c2f20305348ef5e4e14fbb3c31252f06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 20:53:12 +0100 Subject: [PATCH 40/51] chore: improve help wording --- docs/commands/hyperopt.md | 9 +++++---- freqtrade/commands/cli_options.py | 14 ++++++++++---- freqtrade/constants.py | 3 +-- freqtrade/optimize/hyperopt_tools.py | 1 + 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/commands/hyperopt.md b/docs/commands/hyperopt.md index 0a93deae6..9348e15da 100644 --- a/docs/commands/hyperopt.md +++ b/docs/commands/hyperopt.md @@ -55,10 +55,11 @@ options: -e INT, --epochs INT Specify number of epochs (default: 100). --spaces SPACES [SPACES ...] Specify which parameters to hyperopt. Space-separated - list. Available options: all, buy, sell, roi, - stoploss, trailing, protection, trades, default. - Default: `default` - which includes all spaces except - for 'trailing', 'protection', and 'trades'. + list. Available builtin options (custom spaces will + not be listed here): default, all, buy, sell, roi, + stoploss, trailing, protection, trades. Default: + `default` - which includes all spaces except for + 'trailing', 'protection', and 'trades'. --print-all Print all results, not only the best ones. --print-json Print output in JSON format. -j JOBS, --job-workers JOBS diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 1c1a9614f..c256c46f3 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -5,7 +5,10 @@ Definition of cli arguments used in arguments.py from argparse import ArgumentTypeError from freqtrade import constants -from freqtrade.constants import HYPEROPT_BUILTIN_SPACES, HYPEROPT_LOSS_BUILTIN +from freqtrade.constants import ( + HYPEROPT_BUILTIN_SPACE_OPTIONS, + HYPEROPT_LOSS_BUILTIN, +) from freqtrade.enums import CandleType @@ -278,9 +281,12 @@ AVAILABLE_CLI_OPTIONS = { ), "spaces": Arg( "--spaces", - help="Specify which parameters to hyperopt. Space-separated list. Available options: " - f"{', '.join(HYPEROPT_BUILTIN_SPACES)}. Default: `default` - " - "which includes all spaces except for 'trailing', 'protection', and 'trades'.", + help=( + "Specify which parameters to hyperopt. Space-separated list. " + "Available builtin options (custom spaces will not be listed here): " + f"{', '.join(HYPEROPT_BUILTIN_SPACE_OPTIONS)}. Default: `default` - " + "which includes all spaces except for 'trailing', 'protection', and 'trades'." + ), nargs="+", ), "analyze_per_epoch": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c25895957..a386dba28 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -42,7 +42,6 @@ HYPEROPT_LOSS_BUILTIN = [ "MultiMetricHyperOptLoss", ] HYPEROPT_BUILTIN_SPACES = [ - "all", "buy", "sell", "roi", @@ -50,8 +49,8 @@ HYPEROPT_BUILTIN_SPACES = [ "trailing", "protection", "trades", - "default", ] +HYPEROPT_BUILTIN_SPACE_OPTIONS = ["default", "all"] + HYPEROPT_BUILTIN_SPACES AVAILABLE_PAIRLISTS = [ "StaticPairList", diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index d343f6ee5..fbf6448f2 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -220,6 +220,7 @@ class HyperoptTools: else: all_spaces = list(params.keys() | non_optimized.keys()) + # Explicitly listed to keep original sort order spaces = ["buy", "sell", "protection", "roi", "stoploss", "trailing", "max_open_trades"] spaces += [s for s in all_spaces if s not in spaces] lookup = { From 1328df772bf133ed05e53751a385208981282719 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 5 Nov 2025 21:05:15 +0100 Subject: [PATCH 41/51] feat: ensure spaces are valid identifiers --- freqtrade/strategy/hyper.py | 4 ++++ tests/strategy/test_interface.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 847aac102..123600c55 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -192,6 +192,10 @@ def detect_all_parameters( raise OperationalException( f"Conflicting parameter space for {attr_name}: {auto_category} vs {attr.category}." ) + if attr.category in ("all", "default") or attr.category.isidentifier() is False: + raise OperationalException( + f"'{attr.category}' is not a valid space. Parameter: {attr_name}." + ) attr.name = attr_name result[attr.category][attr_name] = attr return result diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bf422d0ec..5f3c11821 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -952,6 +952,7 @@ def test_auto_hyperopt_interface(default_conf): with pytest.raises(OperationalException, match=r"Conflicting parameter space.*"): detect_all_parameters(strategy.__class__) + del strategy.__class__.sell_rsi strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5) @@ -960,6 +961,20 @@ def test_auto_hyperopt_interface(default_conf): ): detect_all_parameters(strategy.__class__) + # Invalid parameter space + strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5, space="all") + with pytest.raises( + OperationalException, match=r"'all' is not a valid space\. Parameter: exit22_rsi\." + ): + detect_all_parameters(strategy.__class__) + + strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5, space="hello:world:22") + with pytest.raises( + OperationalException, + match=r"'hello:world:22' is not a valid space\. Parameter: exit22_rsi\.", + ): + detect_all_parameters(strategy.__class__) + def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): default_conf.update({"strategy": "HyperoptableStrategy"}) From ff4230af8a59cb9e0568321d42d301e800dccab8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 19:43:35 +0100 Subject: [PATCH 42/51] chore: update space parameter docstring --- freqtrade/strategy/parameters.py | 42 +++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/freqtrade/strategy/parameters.py b/freqtrade/strategy/parameters.py index 4bfa91e4a..0e78d95b3 100644 --- a/freqtrade/strategy/parameters.py +++ b/freqtrade/strategy/parameters.py @@ -49,9 +49,9 @@ class BaseParameter(ABC): ): """ Initialize hyperopt-optimizable parameter. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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. @@ -109,8 +109,9 @@ class NumericParameter(BaseParameter): :param high: Upper end (inclusive) of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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.*. @@ -151,8 +152,9 @@ class IntParameter(NumericParameter): :param high: Upper end (inclusive) of optimization space. Must be none of entire range is passed first parameter. :param default: A default value. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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.IntDistribution. @@ -205,8 +207,9 @@ class RealParameter(NumericParameter): :param high: Upper end (inclusive) of optimization space. Must be none if entire range is passed first parameter. :param default: A default value. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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.FloatDistribution. @@ -245,8 +248,9 @@ class DecimalParameter(NumericParameter): Must be none if entire range is passed first parameter. :param default: A default value. :param decimals: A number of decimals after floating point to be included in testing. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter fieldname is prefixed with 'buy_' or 'sell_'. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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's NumericParameter. @@ -310,10 +314,10 @@ class CategoricalParameter(BaseParameter): Initialize hyperopt-optimizable parameter. :param categories: Optimization space, [a, b, ...]. :param default: A default value. If not specified, first item from specified space will be - used. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + used. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter name is prefixed with 'buy_' or 'sell_'. :param optimize: Include parameter in hyperopt optimizations. :param load: Load parameter value from {space}_params. :param kwargs: Compatibility. Optuna's CategoricalDistribution does not @@ -361,10 +365,10 @@ class BooleanParameter(CategoricalParameter): Initialize hyperopt-optimizable Boolean Parameter. It's a shortcut to `CategoricalParameter([True, False])`. :param default: A default value. If not specified, first item from specified space will be - used. - :param space: A parameter category. Can be 'buy' or 'sell'. This parameter is optional if - parameter field - name is prefixed with 'buy_' or 'sell_'. + used. + :param space: The parameter space. Can be 'buy', 'sell', or a string that's also a + valid python identifier. + This parameter is optional if parameter 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. From a3efba019fc2da2d897f0569006ebc560d93518a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:06:52 +0100 Subject: [PATCH 43/51] docs: update documentation for new hyperopt spaces --- docs/hyperopt.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2d5f66df0..141843ac8 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -46,6 +46,8 @@ Depending on the space you want to optimize, only some of the below are required * define parameters with `space='buy'` - for entry signal optimization * define parameters with `space='sell'` - for exit signal optimization +* define parameters with `space='protection'` - for protection optimization +* define parameters with `space='random_spacename'` - for better control over which parameters are optimized together !!! Note `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. @@ -79,15 +81,15 @@ Based on the loss function result, hyperopt will determine the next set of param ### Configure your Guards and Triggers -There are two places you need to change in your strategy file to add a new buy hyperopt for testing: +There are two places you need to change in your strategy file to add a new hyperopt parameter for optimization: * Define the parameters at the class level hyperopt shall be optimizing. * Within `populate_entry_trend()` - use defined parameter values instead of raw constants. There you have two different types of indicators: 1. `guards` and 2. `triggers`. -1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. -2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". +1. Guards are conditions like "never enter if ADX < 10", or never enter if current price is over EMA10. +2. Triggers are ones that actually trigger entry in specific moment, like "enter when EMA5 crosses over EMA10" or "enter when close price touches lower Bollinger band". !!! Hint "Guards and Triggers" Technically, there is no difference between Guards and Triggers. @@ -160,9 +162,10 @@ We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. !!! Note "Parameter space assignment" - Parameters must either be assigned to a variable named `buy_*` or `sell_*` - or contain `space='buy'` | `space='sell'` to be assigned to a space correctly. + Parameters must either be assigned to a variable named `buy_*`, `sell_*` or `protection_*` - or contain have a space assigned explicitly via parameter (`space='buy'`, `space='sell'`, `space='protection'`). If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. Parameters with unclear space (e.g. `adx_period = IntParameter(4, 24, default=14)` - no explicit nor implicit space) will not be detected and will therefore be ignored. + Spaces can also be custom named (e.g. `space='my_custom_space'`), with the only limitation that the space name cannot be `all`, `default` - and must result in a valid python identifier. So let's write the buy strategy using these values: @@ -520,13 +523,13 @@ freqtrade hyperopt --strategy --timerange 20210101-20210201 ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. -Often it might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. +Letting Hyperopt optimize everything is often a huuuuge search space. +Often it might make more sense to start by just searching for initial entry algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new strategy you have. Legal values are: -* `all`: optimize everything +* `all`: optimize everything (including custom spaces) * `buy`: just search for a new buy strategy * `sell`: just search for a new sell strategy * `roi`: just optimize the minimal profit table for your strategy @@ -535,6 +538,7 @@ Legal values are: * `trades`: search for the best max open trades values * `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these) * `default`: `all` except `trailing`, `trades` and `protection` +* `custom_space_name`: any custom space used by any parameter in your strategy * space-separated list of any of the above values for example `--spaces roi stoploss` The default Hyperopt Search Space, used when no `--space` command line option is specified, does not include the `trailing` hyperspace. We recommend you to run optimization for the `trailing` hyperspace separately, when the best parameters for other hyperspaces were found, validated and pasted into your custom strategy. From 649aff8076c7c024234c077c272708785f2752b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:30:04 +0100 Subject: [PATCH 44/51] feat: add builtin spaces enter and exit --- docs/commands/hyperopt.md | 8 ++++---- freqtrade/constants.py | 2 ++ freqtrade/optimize/hyperopt/hyperopt_auto.py | 4 +++- freqtrade/strategy/hyper.py | 2 +- tests/strategy/test_interface.py | 10 ++++++++++ 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/commands/hyperopt.md b/docs/commands/hyperopt.md index 9348e15da..4d9a75b22 100644 --- a/docs/commands/hyperopt.md +++ b/docs/commands/hyperopt.md @@ -56,10 +56,10 @@ options: --spaces SPACES [SPACES ...] Specify which parameters to hyperopt. Space-separated list. Available builtin options (custom spaces will - not be listed here): default, all, buy, sell, roi, - stoploss, trailing, protection, trades. Default: - `default` - which includes all spaces except for - 'trailing', 'protection', and 'trades'. + not be listed here): default, all, buy, sell, enter, + exit, roi, stoploss, trailing, protection, trades. + Default: `default` - which includes all spaces except + for 'trailing', 'protection', and 'trades'. --print-all Print all results, not only the best ones. --print-json Print output in JSON format. -j JOBS, --job-workers JOBS diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a386dba28..5a57f773f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -44,6 +44,8 @@ HYPEROPT_LOSS_BUILTIN = [ HYPEROPT_BUILTIN_SPACES = [ "buy", "sell", + "enter", + "exit", "roi", "stoploss", "trailing", diff --git a/freqtrade/optimize/hyperopt/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py index 603d0b139..fd2a75c55 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -67,7 +67,9 @@ class HyperOptAuto(IHyperOpt): if attr.optimize: yield attr.get_space(attr_name) - def get_indicator_space(self, category: Literal["buy", "sell", "protection"] | str) -> list: + def get_indicator_space( + self, category: Literal["buy", "sell", "enter", "exit", "protection"] | str + ) -> list: """ Get indicator space for a given space. :param category: parameter space to get. diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 123600c55..445a1a300 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -166,7 +166,7 @@ def detect_all_parameters( :param obj: Strategy object or class :return: Dictionary of detected parameters by space """ - auto_categories = ["buy", "sell", "protection"] + auto_categories = ["buy", "sell", "enter", "exit", "protection"] result: AllSpaceParams = defaultdict(dict) for attr_name in dir(obj): if attr_name.startswith("__"): # Ignore internals diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5f3c11821..e536e335a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -974,6 +974,16 @@ def test_auto_hyperopt_interface(default_conf): match=r"'hello:world:22' is not a valid space\. Parameter: exit22_rsi\.", ): detect_all_parameters(strategy.__class__) + del strategy.__class__.exit22_rsi + + # Valid exit parameter + strategy.__class__.exit_rsi = IntParameter([0, 10], default=5) + strategy.__class__.enter_rsi = IntParameter([0, 10], default=5) + spaces = detect_all_parameters(strategy.__class__) + assert "exit" in spaces + assert "enter" in spaces + del strategy.__class__.exit_rsi + del strategy.__class__.enter_rsi def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): From b32ba68a6e9f628a64fc18975bf3402ff48c6619 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:33:07 +0100 Subject: [PATCH 45/51] docs: at enter/exit spaces as builtin spaces --- docs/hyperopt.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 141843ac8..c385de158 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -46,12 +46,17 @@ Depending on the space you want to optimize, only some of the below are required * define parameters with `space='buy'` - for entry signal optimization * define parameters with `space='sell'` - for exit signal optimization +* define parameters with `space='enter'` - for entry signal optimization +* define parameters with `space='exit'` - for exit signal optimization * define parameters with `space='protection'` - for protection optimization * define parameters with `space='random_spacename'` - for better control over which parameters are optimized together +Pick the space name that suits the parameter best. We recommend to use either `buy` / `sell` or `enter` / `exit` for clarity (however there's no technical limitation in this regard). + !!! Note `populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work. + Rarely you may also need to create a [nested class](advanced-hyperopt.md#overriding-pre-defined-spaces) named `HyperOpt` and implement * `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) @@ -532,6 +537,8 @@ Legal values are: * `all`: optimize everything (including custom spaces) * `buy`: just search for a new buy strategy * `sell`: just search for a new sell strategy +* `enter`: just search for a new entry logic +* `exit`: just search for a new entry logic * `roi`: just optimize the minimal profit table for your strategy * `stoploss`: search for the best stoploss value * `trailing`: search for the best trailing stop values From f93c90661446b91ab69987a16d209e8784631f38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:36:40 +0100 Subject: [PATCH 46/51] test: improve tests for new builtin spaces --- tests/optimize/test_hyperopt.py | 6 +++++- tests/strategy/strats/hyperoptable_strategy.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 8ea203bb4..7f986a3d6 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -569,6 +569,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "buy_rsi": 35, "sell_minusdi": 0.02, "sell_rsi": 75, + "exit_rsi": 7, "exitaaa": 7, "protection_cooldown_lookback": 20, "protection_enabled": True, @@ -598,9 +599,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "buy_plusdi": 0.02, "buy_rsi": 35, }, - "exit": { + "exitaspace": { "exitaaa": 7, }, + "exit": { + "exit_rsi": 7, + }, "roi": {"0": 0.12, "20.0": 0.02, "50.0": 0.01, "110.0": 0}, "protection": { "protection_cooldown_lookback": 20, diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 32d1e63bf..73b072e19 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -35,7 +35,9 @@ class HyperoptableStrategy(StrategyTestV3): sell_minusdi = DecimalParameter( low=0, high=1, default=0.5001, decimals=3, space="sell", load=False ) - exitaaa = IntParameter(low=0, high=10, default=5, space="exit") + exitaaa = IntParameter(low=0, high=10, default=5, space="exitaspace") + + exit_rsi = IntParameter(low=0, high=10, default=5) protection_enabled = BooleanParameter(default=True) protection_cooldown_lookback = IntParameter([0, 50], default=30) From 524ceebcbbd78e8432c6ba131d6bc29d2cc87cc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:43:42 +0100 Subject: [PATCH 47/51] fix: don't restrict spaces - explicitly defined space should win --- freqtrade/strategy/hyper.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 445a1a300..4ee94778c 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -174,24 +174,15 @@ def detect_all_parameters( attr = getattr(obj, attr_name) if not issubclass(attr.__class__, BaseParameter): continue - auto_category: str | None = None - # Category auto detection - for category in auto_categories: - if attr_name.startswith(category + "_"): - auto_category = category - break - if auto_category is None and attr.category is None: + if not attr.category: + # Category auto detection + for category in auto_categories: + if attr_name.startswith(category + "_"): + attr.category = category + break + if attr.category is None: raise OperationalException(f"Cannot determine parameter space for {attr_name}.") - if auto_category is not None and attr.category is None: - attr.category = auto_category - if ( - auto_category is not None - and attr.category is not None - and auto_category != attr.category - ): - raise OperationalException( - f"Conflicting parameter space for {attr_name}: {auto_category} vs {attr.category}." - ) + if attr.category in ("all", "default") or attr.category.isidentifier() is False: raise OperationalException( f"'{attr.category}' is not a valid space. Parameter: {attr_name}." From 4a225cab23f4c78131e542d9fc8c5876cb0d23eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:43:57 +0100 Subject: [PATCH 48/51] test: update test for new "conflicting" behavior --- tests/strategy/test_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e536e335a..1cad0e2b4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -950,8 +950,9 @@ def test_auto_hyperopt_interface(default_conf): strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space="buy") - with pytest.raises(OperationalException, match=r"Conflicting parameter space.*"): - detect_all_parameters(strategy.__class__) + spaces = detect_all_parameters(strategy.__class__) + assert "buy" in spaces + assert spaces["buy"]["sell_rsi"] == strategy.sell_rsi del strategy.__class__.sell_rsi strategy.__class__.exit22_rsi = IntParameter([0, 10], default=5) From 5e74700e3191a873c732a3ebf5d82aae49e38c4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:45:33 +0100 Subject: [PATCH 49/51] chore: don't use conflicting spaces in sample strategy --- freqtrade/templates/sample_strategy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 75a0fe385..75a0fc9c2 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -96,7 +96,9 @@ class SampleStrategy(IStrategy): buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True) short_rsi = IntParameter(low=51, high=100, default=70, space="sell", optimize=True, load=True) - exit_short_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True) + exit_short_rsi = IntParameter( + low=1, high=50, default=30, space="exit", optimize=True, load=True + ) # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 200 From 992c2f9e3ed23fe8a0b580ff4947bcd08a844108 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:51:30 +0100 Subject: [PATCH 50/51] feat: improve code for list-strategies --- freqtrade/commands/list_commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 8bd0930f1..a918afae3 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -120,12 +120,13 @@ def _print_objs_tabular(objs: list, print_colorized: bool) -> None: for space, params in s["hyperoptable"].items() if space not in ["buy", "sell", "protection"] ] + hyp = s["hyperoptable"] objs_to_print[idx].update( { - "hyperoptable": "Yes" if len(s["hyperoptable"]) > 0 else "No", - "buy-Params": str(len(s["hyperoptable"].get("buy", []))), - "sell-Params": str(len(s["hyperoptable"].get("sell", []))), - "protection-Params": str(len(s["hyperoptable"].get("protection", []))), + "hyperoptable": "Yes" if len(hyp) > 0 else "No", + "buy-Params": str(len(hyp.get("buy", []))), + "sell-Params": str(len(hyp.get("sell", []))), + "protection-Params": str(len(hyp.get("protection", []))), "custom-Params": ", ".join(custom_params) if custom_params else "", } ) From 6ea83ba0e743e8461fd5427369f67f61a1e93692 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 6 Nov 2025 20:58:12 +0100 Subject: [PATCH 51/51] docs: clarify space naming prevalence --- docs/hyperopt.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index c385de158..7857f5de4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -167,8 +167,9 @@ We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. !!! Note "Parameter space assignment" - Parameters must either be assigned to a variable named `buy_*`, `sell_*` or `protection_*` - or contain have a space assigned explicitly via parameter (`space='buy'`, `space='sell'`, `space='protection'`). - If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. + - Parameters must either be assigned to a variable named `buy_*`, `sell_*`, `enter_*` or `exit_*` or `protection_*` - or contain have a space assigned explicitly via parameter (`space='buy'`, `space='sell'`, `space='protection'`). + - Parameters with conflicting assignments (e.g. `buy_adx = IntParameter(4, 24, default=14, space='sell')`) will use the explicit space assignment. + - If no parameter is available for a space, you'll receive the error that no space was found when running hyperopt. Parameters with unclear space (e.g. `adx_period = IntParameter(4, 24, default=14)` - no explicit nor implicit space) will not be detected and will therefore be ignored. Spaces can also be custom named (e.g. `space='my_custom_space'`), with the only limitation that the space name cannot be `all`, `default` - and must result in a valid python identifier.