Skip to content

Commit d8c2466

Browse files
authored
Merge pull request freqtrade#11736 from viotemp1/optuna_addons
add early stopping for hyperopt
2 parents 195c15c + 5a2b3d9 commit d8c2466

File tree

8 files changed

+85
-0
lines changed

8 files changed

+85
-0
lines changed

docs/commands/hyperopt.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ usage: freqtrade hyperopt [-h] [-v] [--no-color] [--logfile FILE] [-V]
1616
[--random-state INT] [--min-trades INT]
1717
[--hyperopt-loss NAME] [--disable-param-export]
1818
[--ignore-missing-spaces] [--analyze-per-epoch]
19+
[--early-stop INT]
1920
2021
options:
2122
-h, --help show this help message and exit
@@ -87,6 +88,8 @@ options:
8788
Suppress errors for any requested Hyperopt spaces that
8889
do not contain any parameters.
8990
--analyze-per-epoch Run populate_indicators once per epoch.
91+
--early-stop INT Early stop hyperopt if no improvement after (default:
92+
0) epochs.
9093
9194
Common arguments:
9295
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

docs/hyperopt.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ freqtrade hyperopt --config config.json --hyperopt-loss <hyperoptlossname> --str
490490
```
491491

492492
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.
493+
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`)
494+
493495
Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results.
494496

495497
The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below.

freqtrade/commands/arguments.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"disableparamexport",
7979
"hyperopt_ignore_missing_space",
8080
"analyze_per_epoch",
81+
"early_stop",
8182
]
8283

8384
ARGS_EDGE = [*ARGS_COMMON_OPTIMIZE, "stoploss_range"]

freqtrade/commands/cli_options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ def __init__(self, *args, **kwargs):
262262
metavar="INT",
263263
default=constants.HYPEROPT_EPOCH,
264264
),
265+
"early_stop": Arg(
266+
"--early-stop",
267+
help="Early stop hyperopt if no improvement after (default: %(default)d) epochs.",
268+
type=check_int_positive,
269+
metavar="INT",
270+
default=0, # 0 to disable by default
271+
),
265272
"spaces": Arg(
266273
"--spaces",
267274
help="Specify which parameters to hyperopt. Space-separated list.",

freqtrade/configuration/configuration.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,19 @@ def _process_optimize_options(self, config: Config) -> None:
334334
("print_all", "Parameter --print-all detected ..."),
335335
]
336336
self._args_to_config_loop(config, configurations)
337+
es_epochs = self.args.get("early_stop", 0)
338+
if es_epochs > 0:
339+
if es_epochs < 20:
340+
logger.warning(
341+
f"Early stop epochs {es_epochs} lower than 20. It will be replaced with 20."
342+
)
343+
config.update({"early_stop": 20})
344+
else:
345+
config.update({"early_stop": self.args["early_stop"]})
346+
logger.info(
347+
f"Parameter --early-stop detected ... Will early stop hyperopt if no improvement "
348+
f"after {config.get('early_stop')} epochs ..."
349+
)
337350

338351
configurations = [
339352
("print_json", "Parameter --print-json detected ..."),

freqtrade/optimize/hyperopt/hyperopt.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,13 @@ def start(self) -> None:
317317
logging_mp_handle(log_queue)
318318
gc.collect()
319319

320+
if (
321+
self.hyperopter.es_epochs > 0
322+
and self.hyperopter.es_terminator.should_terminate(self.opt)
323+
):
324+
logger.info(f"Early stopping after {(i + 1) * jobs} epochs")
325+
break
326+
320327
except KeyboardInterrupt:
321328
print("User interrupted..")
322329

freqtrade/optimize/hyperopt/hyperopt_optimizer.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from joblib import delayed, dump, load, wrap_non_picklable_objects
1515
from joblib.externals import cloudpickle
1616
from optuna.exceptions import ExperimentalWarning
17+
from optuna.terminator import BestValueStagnationEvaluator, Terminator
1718
from pandas import DataFrame
1819

1920
from freqtrade.constants import DATETIME_PRINT_FORMAT, Config
@@ -104,6 +105,10 @@ def __init__(self, config: Config, data_pickle_file: Path) -> None:
104105

105106
self.market_change = 0.0
106107

108+
self.es_epochs = config.get("early_stop", 0)
109+
if self.es_epochs > 0 and self.es_epochs < 0.2 * config.get("epochs", 0):
110+
logger.warning(f"Early stop epochs {self.es_epochs} lower than 20% of total epochs")
111+
107112
if HyperoptTools.has_space(self.config, "sell"):
108113
# Make sure use_exit_signal is enabled
109114
self.config["use_exit_signal"] = True
@@ -424,6 +429,11 @@ def get_optimizer(
424429
else:
425430
sampler = o_sampler
426431

432+
if self.es_epochs > 0:
433+
with warnings.catch_warnings():
434+
warnings.filterwarnings(action="ignore", category=ExperimentalWarning)
435+
self.es_terminator = Terminator(BestValueStagnationEvaluator(self.es_epochs))
436+
427437
logger.info(f"Using optuna sampler {o_sampler}.")
428438
return optuna.create_study(sampler=sampler, direction="minimize")
429439

tests/optimize/test_hyperopt.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,48 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None
170170
setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
171171

172172

173+
def test_setup_hyperopt_early_stop_setup(mocker, default_conf, caplog) -> None:
174+
patched_configuration_load_config_file(mocker, default_conf)
175+
176+
args = [
177+
"hyperopt",
178+
"--config",
179+
"config.json",
180+
"--strategy",
181+
"HyperoptableStrategy",
182+
"--early-stop",
183+
"1",
184+
]
185+
conf = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
186+
assert isinstance(conf, dict)
187+
assert conf["early_stop"] == 20
188+
msg = (
189+
r"Parameter --early-stop detected ... "
190+
r"Will early stop hyperopt if no improvement after (20|25) epochs ..."
191+
)
192+
msg_adjust = r"Early stop epochs .* lower than 20. It will be replaced with 20."
193+
assert log_has_re(msg_adjust, caplog)
194+
assert log_has_re(msg, caplog)
195+
196+
caplog.clear()
197+
198+
args = [
199+
"hyperopt",
200+
"--config",
201+
"config.json",
202+
"--strategy",
203+
CURRENT_TEST_STRATEGY,
204+
"--early-stop",
205+
"25",
206+
]
207+
conf1 = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT)
208+
assert isinstance(conf1, dict)
209+
210+
assert conf1["early_stop"] == 25
211+
assert not log_has_re(msg_adjust, caplog)
212+
assert log_has_re(msg, caplog)
213+
214+
173215
def test_start_not_installed(mocker, default_conf, import_fails) -> None:
174216
start_mock = MagicMock()
175217
patched_configuration_load_config_file(mocker, default_conf)

0 commit comments

Comments
 (0)