From 442a1ba50d57c9ac69552c1abea1f3d0e5126a5d Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 10 May 2025 19:36:48 +0300 Subject: [PATCH 1/7] add early stopping for hyperopt --- docs/hyperopt.md | 2 ++ freqtrade/commands/arguments.py | 1 + freqtrade/commands/cli_options.py | 8 ++++++++ freqtrade/configuration/configuration.py | 5 +++++ freqtrade/optimize/hyperopt/hyperopt.py | 7 +++++++ freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 11 +++++++++++ 6 files changed, 34 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3a8100972..f209b9528 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -490,6 +490,8 @@ freqtrade hyperopt --config config.json --hyperopt-loss --str ``` The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +The `-es` option will set ofter how many batches of evaluations with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`-es=0`) + Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index c42e46711..52beb3dfa 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -78,6 +78,7 @@ ARGS_HYPEROPT = [ "disableparamexport", "hyperopt_ignore_missing_space", "analyze_per_epoch", + "early_stop", ] ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f05907808..12b5ccb5b 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -262,6 +262,14 @@ AVAILABLE_CLI_OPTIONS = { metavar="INT", default=constants.HYPEROPT_EPOCH, ), + "early_stop": Arg( + "-es", + "--early-stop", + help="Early stop hyperopt if no improvement after (default: %(default)d) epochs.", + type=check_int_positive, + metavar="INT", + default=0, # 0 to disable by default + ), "spaces": Arg( "--spaces", help="Specify which parameters to hyperopt. Space-separated list.", diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 11aff879f..6cde18816 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -334,6 +334,11 @@ class Configuration: ("print_all", "Parameter --print-all detected ..."), ] self._args_to_config_loop(config, configurations) + if self.args.get("early_stop", 0) > 0: + logger.info( + f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement " + f"after {self.args.get('early_stop')} epochs ..." + ) configurations = [ ("print_json", "Parameter --print-json detected ..."), diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 3db5ec9fe..3ddee162c 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -317,6 +317,13 @@ class Hyperopt: logging_mp_handle(log_queue) gc.collect() + if ( + self.hyperopter.es_batches > 0 + and self.hyperopter.es_terminator.should_terminate(self.opt) + ): + logger.info(f"Early stopping after {(i + 1) * jobs} epochs") + break + except KeyboardInterrupt: print("User interrupted..") diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 99e81e4b3..467475b6d 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -14,6 +14,7 @@ import optuna from joblib import delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from optuna.exceptions import ExperimentalWarning +from optuna.terminator import BestValueStagnationEvaluator, Terminator from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, Config @@ -104,6 +105,11 @@ class HyperOptimizer: self.market_change = 0.0 + self.es_epochs = config.get("early_stop", 0) + self.es_batches = self.es_epochs // config.get("hyperopt_jobs", 1) + if self.es_epochs > 0 and self.es_epochs < 0.2 * config.get("epochs", 0): + logger.warning(f"Easly stop epochs {self.es_epochs} lower than 20% of total epochs") + if HyperoptTools.has_space(self.config, "sell"): # Make sure use_exit_signal is enabled self.config["use_exit_signal"] = True @@ -424,6 +430,11 @@ class HyperOptimizer: else: sampler = o_sampler + if self.es_batches > 0: + with warnings.catch_warnings(): + warnings.filterwarnings(action="ignore", category=ExperimentalWarning) + self.es_terminator = Terminator(BestValueStagnationEvaluator(self.es_batches)) + logger.info(f"Using optuna sampler {o_sampler}.") return optuna.create_study(sampler=sampler, direction="minimize") From 5b92af6a908cf3782a1a8c82d146bc2e2e1dc799 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 10 May 2025 19:45:16 +0300 Subject: [PATCH 2/7] fix some spelling errors --- docs/hyperopt.md | 2 +- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index f209b9528..e930f4bf4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -490,7 +490,7 @@ freqtrade hyperopt --config config.json --hyperopt-loss --str ``` The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -The `-es` option will set ofter how many batches of evaluations with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`-es=0`) +The `-es` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`-es=0`) Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 467475b6d..dd09bf318 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -108,7 +108,7 @@ class HyperOptimizer: self.es_epochs = config.get("early_stop", 0) self.es_batches = self.es_epochs // config.get("hyperopt_jobs", 1) if self.es_epochs > 0 and self.es_epochs < 0.2 * config.get("epochs", 0): - logger.warning(f"Easly stop epochs {self.es_epochs} lower than 20% of total epochs") + logger.warning(f"Early stop epochs {self.es_epochs} lower than 20% of total epochs") if HyperoptTools.has_space(self.config, "sell"): # Make sure use_exit_signal is enabled From 28e5efc902f5238e0ae9814fb426ac0f45356804 Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 17 May 2025 01:15:10 +0300 Subject: [PATCH 3/7] fix docs --- docs/commands/hyperopt.md | 3 +++ docs/hyperopt.md | 2 +- freqtrade/commands/cli_options.py | 1 - freqtrade/configuration/configuration.py | 1 + freqtrade/optimize/hyperopt/hyperopt.py | 2 +- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 5 ++--- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/commands/hyperopt.md b/docs/commands/hyperopt.md index e7a05bedf..f1c8b28b5 100644 --- a/docs/commands/hyperopt.md +++ b/docs/commands/hyperopt.md @@ -16,6 +16,7 @@ usage: freqtrade hyperopt [-h] [-v] [--no-color] [--logfile FILE] [-V] [--random-state INT] [--min-trades INT] [--hyperopt-loss NAME] [--disable-param-export] [--ignore-missing-spaces] [--analyze-per-epoch] + [--early-stop INT] options: -h, --help show this help message and exit @@ -87,6 +88,8 @@ options: Suppress errors for any requested Hyperopt spaces that do not contain any parameters. --analyze-per-epoch Run populate_indicators once per epoch. + --early-stop INT Early stop hyperopt if no improvement after (default: + 0) epochs. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e930f4bf4..0c1a53421 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -490,7 +490,7 @@ freqtrade hyperopt --config config.json --hyperopt-loss --str ``` The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -The `-es` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`-es=0`) +The `--early-stop` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`--early-stop=0`) Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 12b5ccb5b..c5d0ad49b 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -263,7 +263,6 @@ AVAILABLE_CLI_OPTIONS = { default=constants.HYPEROPT_EPOCH, ), "early_stop": Arg( - "-es", "--early-stop", help="Early stop hyperopt if no improvement after (default: %(default)d) epochs.", type=check_int_positive, diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 6cde18816..999b31fe5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -335,6 +335,7 @@ class Configuration: ] self._args_to_config_loop(config, configurations) if self.args.get("early_stop", 0) > 0: + config.update({"early_stop": self.args["early_stop"]}) logger.info( f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement " f"after {self.args.get('early_stop')} epochs ..." diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 3ddee162c..926aaaf8c 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -318,7 +318,7 @@ class Hyperopt: gc.collect() if ( - self.hyperopter.es_batches > 0 + self.hyperopter.es_epochs > 0 and self.hyperopter.es_terminator.should_terminate(self.opt) ): logger.info(f"Early stopping after {(i + 1) * jobs} epochs") diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index dd09bf318..d0346f23a 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -106,7 +106,6 @@ class HyperOptimizer: self.market_change = 0.0 self.es_epochs = config.get("early_stop", 0) - self.es_batches = self.es_epochs // config.get("hyperopt_jobs", 1) if self.es_epochs > 0 and self.es_epochs < 0.2 * config.get("epochs", 0): logger.warning(f"Early stop epochs {self.es_epochs} lower than 20% of total epochs") @@ -430,10 +429,10 @@ class HyperOptimizer: else: sampler = o_sampler - if self.es_batches > 0: + if self.es_epochs > 0: with warnings.catch_warnings(): warnings.filterwarnings(action="ignore", category=ExperimentalWarning) - self.es_terminator = Terminator(BestValueStagnationEvaluator(self.es_batches)) + self.es_terminator = Terminator(BestValueStagnationEvaluator(self.es_epochs)) logger.info(f"Using optuna sampler {o_sampler}.") return optuna.create_study(sampler=sampler, direction="minimize") From d33e931a0d092a2d3377834ddd96f18261dc69ad Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 17 May 2025 16:23:25 +0300 Subject: [PATCH 4/7] early stop - replace values lower than 20 with 20 and display a warning. --- docs/hyperopt.md | 2 +- freqtrade/configuration/configuration.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0c1a53421..af5ea9d2c 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -490,7 +490,7 @@ freqtrade hyperopt --config config.json --hyperopt-loss --str ``` The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -The `--early-stop` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Early stop is by default disabled (`--early-stop=0`) +The `--early-stop` option will set after how many epochs with no improvements hyperopt will stop. A good value is 20-30% of the total epochs. Any value greater than 0 and lower than 20 it will be replaced by 20. Early stop is by default disabled (`--early-stop=0`) Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 999b31fe5..b5b05a91f 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -334,8 +334,16 @@ class Configuration: ("print_all", "Parameter --print-all detected ..."), ] self._args_to_config_loop(config, configurations) - if self.args.get("early_stop", 0) > 0: - config.update({"early_stop": self.args["early_stop"]}) + es_epochs = self.args.get("early_stop", 0) + if es_epochs > 0: + if es_epochs < 20: + logger.warning( + f"Early stop epochs {es_epochs} lower than 20. " + f"It will be replaced with 20." + ) + config.update({"early_stop": 20}) + else: + config.update({"early_stop": self.args["early_stop"]}) logger.info( f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement " f"after {self.args.get('early_stop')} epochs ..." From bd1a12ceaa0bcc6b10f30f0db6397e1882c1e6ab Mon Sep 17 00:00:00 2001 From: viotemp1 Date: Sat, 17 May 2025 16:27:33 +0300 Subject: [PATCH 5/7] fix formatting --- freqtrade/configuration/configuration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index b5b05a91f..74b12d210 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -338,8 +338,7 @@ class Configuration: if es_epochs > 0: if es_epochs < 20: logger.warning( - f"Early stop epochs {es_epochs} lower than 20. " - f"It will be replaced with 20." + f"Early stop epochs {es_epochs} lower than 20. It will be replaced with 20." ) config.update({"early_stop": 20}) else: From 8641796d045044adb49bc8d2f5ac82fc8aae18cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 May 2025 07:03:41 +0200 Subject: [PATCH 6/7] test: Add test for early-stop config adjustment --- tests/optimize/test_hyperopt.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c22323bdf..4d9d68eae 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -170,6 +170,48 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) +def test_setup_hyperopt_early_stop_setup(mocker, default_conf, caplog) -> None: + patched_configuration_load_config_file(mocker, default_conf) + + args = [ + "hyperopt", + "--config", + "config.json", + "--strategy", + "HyperoptableStrategy", + "--early-stop", + "1", + ] + conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + assert isinstance(conf, dict) + assert conf["early_stop"] == 20 + msg = ( + r"Parameter --early-stop detected ... " + r"Will early stop hyperopt if no improvement after (20|25) epochs ..." + ) + msg_adjust = r"Early stop epochs .* lower than 20. It will be replaced with 20." + assert log_has_re(msg_adjust, caplog) + assert log_has_re(msg, caplog) + + caplog.clear() + + args = [ + "hyperopt", + "--config", + "config.json", + "--strategy", + CURRENT_TEST_STRATEGY, + "--early-stop", + "25", + ] + conf1 = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) + assert isinstance(conf1, dict) + + assert conf1["early_stop"] == 25 + assert not log_has_re(msg_adjust, caplog) + assert log_has_re(msg, caplog) + + def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) From 5a2b3d9d87e4c1b6bcfb3ede405cfc9117cc5bfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 May 2025 07:04:11 +0200 Subject: [PATCH 7/7] chore: show the actually used value --- freqtrade/configuration/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 74b12d210..264d114b8 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -345,7 +345,7 @@ class Configuration: config.update({"early_stop": self.args["early_stop"]}) logger.info( f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement " - f"after {self.args.get('early_stop')} epochs ..." + f"after {config.get('early_stop')} epochs ..." ) configurations = [