Merge pull request #12479 from freqtrade/feat/hyperopt_custom_spaces

Add support for custom hyperopt spaces
This commit is contained in:
Matthias
2025-11-08 16:01:42 +01:00
committed by GitHub
18 changed files with 332 additions and 284 deletions

View File

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

View File

@@ -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,
}
)
@@ -569,6 +569,8 @@ 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,
"roi_t1": 60.0,
@@ -597,6 +599,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"buy_plusdi": 0.02,
"buy_rsi": 35,
},
"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,
@@ -616,7 +624,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,
}
@@ -906,14 +914,43 @@ 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
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_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={})
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:

View File

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

View File

@@ -35,6 +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="exitaspace")
exit_rsi = IntParameter(low=0, high=10, default=5)
protection_enabled = BooleanParameter(default=True)
protection_cooldown_lookback = IntParameter([0, 50], default=30)

View File

@@ -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
from freqtrade.strategy.parameters import (
IntParameter,
)
@@ -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
@@ -940,20 +939,52 @@ 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
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
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")]
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)
with pytest.raises(
OperationalException, match=r"Cannot determine parameter space for exit22_rsi\."
):
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__)
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):