From e1b3ae208d1b778d58ac404b811cbaf692e2d624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 14:32:45 +0100 Subject: [PATCH 01/15] chore: move hyperopt to it's own module --- freqtrade/optimize/hyperopt/__init__.py | 2 ++ freqtrade/optimize/{ => hyperopt}/hyperopt.py | 0 2 files changed, 2 insertions(+) create mode 100644 freqtrade/optimize/hyperopt/__init__.py rename freqtrade/optimize/{ => hyperopt}/hyperopt.py (100%) diff --git a/freqtrade/optimize/hyperopt/__init__.py b/freqtrade/optimize/hyperopt/__init__.py new file mode 100644 index 000000000..2c875027d --- /dev/null +++ b/freqtrade/optimize/hyperopt/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.optimize.hyperopt.hyperopt import Hyperopt, IHyperOptLoss diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py similarity index 100% rename from freqtrade/optimize/hyperopt.py rename to freqtrade/optimize/hyperopt/hyperopt.py From 84fc5dfcf74d9d3ca77261299e3b705b241b6d85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 14:39:42 +0100 Subject: [PATCH 02/15] refactor: move hyperopt-loss interface --- freqtrade/optimize/hyperopt/__init__.py | 3 ++- freqtrade/optimize/hyperopt/hyperopt.py | 2 +- .../optimize/{ => hyperopt_loss}/hyperopt_loss_interface.py | 0 freqtrade/resolvers/hyperopt_resolver.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename freqtrade/optimize/{ => hyperopt_loss}/hyperopt_loss_interface.py (100%) diff --git a/freqtrade/optimize/hyperopt/__init__.py b/freqtrade/optimize/hyperopt/__init__.py index 2c875027d..3ddb091b8 100644 --- a/freqtrade/optimize/hyperopt/__init__.py +++ b/freqtrade/optimize/hyperopt/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa: F401 -from freqtrade.optimize.hyperopt.hyperopt import Hyperopt, IHyperOptLoss +from freqtrade.optimize.hyperopt.hyperopt import Hyperopt +from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 0f46ee1e9..0d29a69ad 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -30,7 +30,7 @@ from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_auto import HyperOptAuto -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import ( HyperoptStateContainer, diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py similarity index 100% rename from freqtrade/optimize/hyperopt_loss_interface.py rename to freqtrade/optimize/hyperopt_loss/hyperopt_loss_interface.py diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 72bbfa886..80c2e7890 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -9,7 +9,7 @@ from pathlib import Path from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS, Config from freqtrade.exceptions import OperationalException -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver From 61d9002cb153f14a1cd9a7407199e27cc4fc5fe5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 14:43:56 +0100 Subject: [PATCH 03/15] refactor: move hyperopt-output --- freqtrade/commands/hyperopt_commands.py | 2 +- freqtrade/optimize/hyperopt/hyperopt.py | 2 +- freqtrade/optimize/{ => hyperopt}/hyperopt_output.py | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename freqtrade/optimize/{ => hyperopt}/hyperopt_output.py (100%) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 4bb33c362..9dfab02c0 100644 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -15,7 +15,7 @@ def start_hyperopt_list(args: dict[str, Any]) -> None: """ from freqtrade.configuration import setup_utils_configuration from freqtrade.data.btanalysis import get_latest_hyperopt_file - from freqtrade.optimize.hyperopt_output import HyperoptOutput + from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import HyperoptTools config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 0d29a69ad..83204d06c 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -27,11 +27,11 @@ from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting +from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss -from freqtrade.optimize.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import ( HyperoptStateContainer, HyperoptTools, diff --git a/freqtrade/optimize/hyperopt_output.py b/freqtrade/optimize/hyperopt/hyperopt_output.py similarity index 100% rename from freqtrade/optimize/hyperopt_output.py rename to freqtrade/optimize/hyperopt/hyperopt_output.py From 851a9a76205a4e25467320f5c5268620efcdb7fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 14:54:16 +0100 Subject: [PATCH 04/15] refactor: move hyperopt-auto --- freqtrade/optimize/hyperopt/hyperopt.py | 4 ++-- freqtrade/optimize/{ => hyperopt}/hyperopt_auto.py | 2 +- freqtrade/optimize/{ => hyperopt}/hyperopt_interface.py | 0 tests/optimize/test_hyperopt.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename freqtrade/optimize/{ => hyperopt}/hyperopt_auto.py (97%) rename freqtrade/optimize/{ => hyperopt}/hyperopt_interface.py (100%) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 83204d06c..c0738b186 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -27,10 +27,10 @@ from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting -from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules -from freqtrade.optimize.hyperopt_auto import HyperOptAuto +from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto +from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_tools import ( HyperoptStateContainer, diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt/hyperopt_auto.py similarity index 97% rename from freqtrade/optimize/hyperopt_auto.py rename to freqtrade/optimize/hyperopt/hyperopt_auto.py index 702cff51d..1da80ec02 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt/hyperopt_auto.py @@ -14,7 +14,7 @@ from freqtrade.exceptions import OperationalException with suppress(ImportError): from skopt.space import Dimension -from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt +from freqtrade.optimize.hyperopt.hyperopt_interface import EstimatorType, IHyperOpt logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt/hyperopt_interface.py similarity index 100% rename from freqtrade/optimize/hyperopt_interface.py rename to freqtrade/optimize/hyperopt/hyperopt_interface.py diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e7b03da6f..150cb0d8d 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -14,7 +14,7 @@ from freqtrade.data.history import load_data from freqtrade.enums import ExitType, RunMode from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.optimize.hyperopt_auto import HyperOptAuto +from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal From 62234878a1cb638aba4bb10b7a85ecac6dca76e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 15:02:17 +0100 Subject: [PATCH 05/15] test: update mocks for new layout --- tests/optimize/test_hyperopt.py | 94 +++++++++++++++++---------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 150cb0d8d..f7b7c93b7 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -222,7 +222,7 @@ def test_start_no_data(mocker, hyperopt_conf, tmp_path) -> None: patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch("freqtrade.data.history.load_pair_history", MagicMock(return_value=pd.DataFrame)) mocker.patch( - "freqtrade.optimize.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -333,21 +333,21 @@ def test_params_no_optimize_details(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) # Dummy-reduce points to ensure scikit-learn is forced to generate new values - mocker.patch("freqtrade.optimize.hyperopt.INITIAL_POINTS", 2) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.INITIAL_POINTS", 2) parallel = mocker.patch( "freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel", @@ -521,15 +521,17 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "final_balance": 1000, } - mocker.patch("freqtrade.optimize.hyperopt.Backtesting.backtest", return_value=backtest_result) mocker.patch( - "freqtrade.optimize.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.Backtesting.backtest", return_value=backtest_result + ) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", return_value=(dt_utc(2017, 12, 10), dt_utc(2017, 12, 13)), ) patch_exchange(mocker) mocker.patch.object(Path, "open") mocker.patch("freqtrade.configuration.config_validation.validate_config_schema") - mocker.patch("freqtrade.optimize.hyperopt.load", return_value={"XRP/BTC": None}) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.load", return_value={"XRP/BTC": None}) optimizer_param = { "buy_plusdi": 0.02, @@ -603,8 +605,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): "freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file", MagicMock(return_value={}), ) - mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) - unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.Path.is_file", MagicMock(return_value=True)) + unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.Path.unlink", MagicMock()) h = Hyperopt(hyperopt_conf) assert unlinkmock.call_count == 2 @@ -612,17 +614,17 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) mocker.patch( "freqtrade.optimize.backtesting.Backtesting.load_bt_data", MagicMock(return_value=(MagicMock(), None)), ) mocker.patch( - "freqtrade.optimize.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -677,16 +679,16 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) mocker.patch( "freqtrade.optimize.backtesting.Backtesting.load_bt_data", MagicMock(return_value=(MagicMock(), None)), ) mocker.patch( - "freqtrade.optimize.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -732,16 +734,16 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -785,16 +787,16 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -835,14 +837,14 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: - mocker.patch("freqtrade.optimize.hyperopt.dump", MagicMock()) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -855,7 +857,8 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: ) mocker.patch( - "freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space", return_value=[] + "freqtrade.optimize.hyperopt.hyperopt_auto.HyperOptAuto._generate_indicator_space", + return_value=[], ) hyperopt = Hyperopt(hyperopt_conf) @@ -873,16 +876,16 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -923,16 +926,16 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.calculate_market_change", return_value=1.5) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -985,18 +988,19 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: ], ) def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: - mocker.patch("freqtrade.optimize.hyperopt.dump", MagicMock()) - mocker.patch("freqtrade.optimize.hyperopt.file_dump_json") + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) mocker.patch( - "freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space", return_value=[] + "freqtrade.optimize.hyperopt.hyperopt_auto.HyperOptAuto._generate_indicator_space", + return_value=[], ) patch_exchange(mocker) @@ -1015,7 +1019,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None patch_exchange(mocker) mocker.patch(f"{EXMS}.get_fee", fee) # Dummy-reduce points to ensure scikit-learn is forced to generate new values - mocker.patch("freqtrade.optimize.hyperopt.INITIAL_POINTS", 2) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.INITIAL_POINTS", 2) (tmp_path / "hyperopt_results").mkdir(parents=True) # No hyperopt needed hyperopt_conf.update( @@ -1063,7 +1067,7 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets())) (tmp_path / "hyperopt_results").mkdir(parents=True) # Dummy-reduce points to ensure scikit-learn is forced to generate new values - mocker.patch("freqtrade.optimize.hyperopt.INITIAL_POINTS", 2) + mocker.patch("freqtrade.optimize.hyperopt.hyperopt.INITIAL_POINTS", 2) # No hyperopt needed hyperopt_conf.update( { From 6719d9670ddf2ff02d6b208a630ffab76873baff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 15:09:47 +0100 Subject: [PATCH 06/15] feat: split hyperopt class this ensures it's clear which parts are passed to workers --- freqtrade/optimize/hyperopt/hyperopt.py | 389 +-------------- .../optimize/hyperopt/hyperopt_optimizer.py | 443 ++++++++++++++++++ 2 files changed, 461 insertions(+), 371 deletions(-) create mode 100644 freqtrade/optimize/hyperopt/hyperopt_optimizer.py diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index c0738b186..61cb4e8f3 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -7,29 +7,24 @@ This module contains the hyperopt logic import logging import random import sys -import warnings -from datetime import datetime, timezone +from datetime import datetime from math import ceil from pathlib import Path from typing import Any import rapidjson -from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects +from joblib import Parallel, cpu_count, delayed, wrap_non_picklable_objects from joblib.externals import cloudpickle -from pandas import DataFrame from rich.console import Console -from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config -from freqtrade.data.converter import trim_dataframes -from freqtrade.data.history import get_timerange -from freqtrade.data.metrics import calculate_market_change +from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException -from freqtrade.misc import deep_merge_dicts, file_dump_json, plural -from freqtrade.optimize.backtesting import Backtesting +from freqtrade.misc import file_dump_json, plural # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto +from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_tools import ( @@ -37,17 +32,9 @@ from freqtrade.optimize.hyperopt_tools import ( HyperoptTools, hyperopt_serializer, ) -from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver from freqtrade.util import get_progress_tracker -# Suppress scikit-learn FutureWarnings from skopt -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - from skopt import Optimizer - from skopt.space import Dimension - logger = logging.getLogger(__name__) @@ -57,8 +44,6 @@ INITIAL_POINTS = 30 # in the skopt model queue, to optimize memory consumption SKOPT_MODEL_QUEUE_SIZE = 10 -MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization - class Hyperopt: """ @@ -70,43 +55,19 @@ class Hyperopt: """ def __init__(self, config: Config) -> None: - self.buy_space: list[Dimension] = [] - self.sell_space: list[Dimension] = [] - self.protection_space: list[Dimension] = [] - self.roi_space: list[Dimension] = [] - self.stoploss_space: list[Dimension] = [] - self.trailing_space: list[Dimension] = [] - self.max_open_trades_space: list[Dimension] = [] - self.dimensions: list[Dimension] = [] - self._hyper_out: HyperoptOutput = HyperoptOutput(streaming=True) self.config = config - self.min_date: datetime - self.max_date: datetime - self.backtesting = Backtesting(self.config) - self.pairlist = self.backtesting.pairlists.whitelist - self.custom_hyperopt: HyperOptAuto self.analyze_per_epoch = self.config.get("analyze_per_epoch", False) HyperoptStateContainer.set_state(HyperoptState.STARTUP) - if not self.config.get("hyperopt"): - self.custom_hyperopt = HyperOptAuto(self.config) - else: + if self.config.get("hyperopt"): raise OperationalException( "Using separate Hyperopt files has been removed in 2021.9. Please convert " "your existing Hyperopt file to the new Hyperoptable strategy interface" ) - self.backtesting._set_strategy(self.backtesting.strategylist[0]) - self.custom_hyperopt.strategy = self.backtesting.strategy - - self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) - self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( - self.config - ) - self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") strategy = str(self.config["strategy"]) self.results_file: Path = ( @@ -123,7 +84,6 @@ class Hyperopt: self.clean_hyperopt() - self.market_change = 0.0 self.num_epochs_saved = 0 self.current_best_epoch: dict[str, Any] | None = None @@ -136,6 +96,8 @@ class Hyperopt: self.print_colorized = self.config.get("print_colorized", False) self.print_json = self.config.get("print_json", False) + self.hyperopter = HyperOptimizer(self.config) + @staticmethod def get_lock_filename(config: Config) -> str: return str(config["user_data_dir"] / "hyperopt.lock") @@ -161,18 +123,6 @@ class Hyperopt: cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) self.hyperopt_pickle_magic(modules.__bases__) - def _get_params_dict( - self, dimensions: list[Dimension], raw_params: list[Any] - ) -> dict[str, Any]: - # Ensure the number of dimensions match - # the number of parameters in the list. - if len(raw_params) != len(dimensions): - raise ValueError("Mismatch in number of search-space dimensions.") - - # Return a dict where the keys are the names of the dimensions - # and the values are taken from the list of parameters. - return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} - def _save_result(self, epoch: dict) -> None: """ Save hyperopt results to file @@ -199,58 +149,6 @@ class Hyperopt: latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {"latest_hyperopt": str(self.results_file.name)}, log=False) - def _get_params_details(self, params: dict) -> dict: - """ - Return the params for each space - """ - result: dict = {} - - if HyperoptTools.has_space(self.config, "buy"): - result["buy"] = {p.name: params.get(p.name) for p in self.buy_space} - if HyperoptTools.has_space(self.config, "sell"): - result["sell"] = {p.name: params.get(p.name) for p in self.sell_space} - if HyperoptTools.has_space(self.config, "protection"): - result["protection"] = {p.name: params.get(p.name) for p in self.protection_space} - if HyperoptTools.has_space(self.config, "roi"): - result["roi"] = { - str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items() - } - if HyperoptTools.has_space(self.config, "stoploss"): - result["stoploss"] = {p.name: params.get(p.name) for p in self.stoploss_space} - if HyperoptTools.has_space(self.config, "trailing"): - result["trailing"] = self.custom_hyperopt.generate_trailing_params(params) - if HyperoptTools.has_space(self.config, "trades"): - result["max_open_trades"] = { - "max_open_trades": ( - self.backtesting.strategy.max_open_trades - if self.backtesting.strategy.max_open_trades != float("inf") - else -1 - ) - } - - return result - - def _get_no_optimize_details(self) -> dict[str, Any]: - """ - Get non-optimized parameters - """ - result: dict[str, Any] = {} - strategy = self.backtesting.strategy - if not HyperoptTools.has_space(self.config, "roi"): - result["roi"] = {str(k): v for k, v in strategy.minimal_roi.items()} - if not HyperoptTools.has_space(self.config, "stoploss"): - result["stoploss"] = {"stoploss": strategy.stoploss} - if not HyperoptTools.has_space(self.config, "trailing"): - result["trailing"] = { - "trailing_stop": strategy.trailing_stop, - "trailing_stop_positive": strategy.trailing_stop_positive, - "trailing_stop_positive_offset": strategy.trailing_stop_positive_offset, - "trailing_only_offset_is_reached": strategy.trailing_only_offset_is_reached, - } - if not HyperoptTools.has_space(self.config, "trades"): - result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades} - return result - def print_results(self, results: dict[str, Any]) -> None: """ Log results if it is better than any previous evaluation @@ -266,258 +164,16 @@ class Hyperopt: self.print_all, ) - def init_spaces(self): - """ - 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() - - 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() - - 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 = ( - self.buy_space - + self.sell_space - + self.protection_space - + self.roi_space - + self.stoploss_space - + self.trailing_space - + self.max_open_trades_space - ) - - 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] - - def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]: - """ - Used Optimize function. - Called once per epoch to optimize whatever is configured. - Keep this function as optimized as possible! - """ - HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) - backtest_start_time = datetime.now(timezone.utc) - params_dict = self._get_params_dict(self.dimensions, raw_params) - - # 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") - - if HyperoptTools.has_space(self.config, "roi"): - self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( - params_dict - ) - - if HyperoptTools.has_space(self.config, "stoploss"): - self.backtesting.strategy.stoploss = params_dict["stoploss"] - - if HyperoptTools.has_space(self.config, "trailing"): - d = self.custom_hyperopt.generate_trailing_params(params_dict) - self.backtesting.strategy.trailing_stop = d["trailing_stop"] - self.backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] - self.backtesting.strategy.trailing_stop_positive_offset = d[ - "trailing_stop_positive_offset" - ] - self.backtesting.strategy.trailing_only_offset_is_reached = d[ - "trailing_only_offset_is_reached" - ] - - if HyperoptTools.has_space(self.config, "trades"): - if self.config["stake_amount"] == "unlimited" and ( - params_dict["max_open_trades"] == -1 or params_dict["max_open_trades"] == 0 - ): - # Ignore unlimited max open trades if stake amount is unlimited - params_dict.update({"max_open_trades": self.config["max_open_trades"]}) - - updated_max_open_trades = ( - int(params_dict["max_open_trades"]) - if (params_dict["max_open_trades"] != -1 and params_dict["max_open_trades"] != 0) - else float("inf") - ) - - self.config.update({"max_open_trades": updated_max_open_trades}) - - self.backtesting.strategy.max_open_trades = updated_max_open_trades - - with self.data_pickle_file.open("rb") as f: - processed = load(f, mmap_mode="r") - if self.analyze_per_epoch: - # Data is not yet analyzed, rerun populate_indicators. - processed = self.advise_and_trim(processed) - - bt_results = self.backtesting.backtest( - processed=processed, start_date=self.min_date, end_date=self.max_date - ) - backtest_end_time = datetime.now(timezone.utc) - bt_results.update( - { - "backtest_start_time": int(backtest_start_time.timestamp()), - "backtest_end_time": int(backtest_end_time.timestamp()), - } - ) - - return self._get_results_dict( - bt_results, self.min_date, self.max_date, params_dict, processed=processed - ) - - def _get_results_dict( - self, - backtesting_results: dict[str, Any], - min_date: datetime, - max_date: datetime, - params_dict: dict[str, Any], - processed: dict[str, DataFrame], - ) -> dict[str, Any]: - params_details = self._get_params_details(params_dict) - - strat_stats = generate_strategy_stats( - self.pairlist, - self.backtesting.strategy.get_strategy_name(), - backtesting_results, - min_date, - max_date, - market_change=self.market_change, - is_hyperopt=True, - ) - results_explanation = HyperoptTools.format_results_explanation_string( - strat_stats, self.config["stake_currency"] - ) - - not_optimized = self.backtesting.strategy.get_no_optimize_params() - not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details()) - - trade_count = strat_stats["total_trades"] - total_profit = strat_stats["profit_total"] - - # If this evaluation contains too short amount of trades to be - # interesting -- consider it as 'bad' (assigned max. loss value) - # in order to cast this hyperspace point away from optimization - # path. We do not want to optimize 'hodl' strategies. - loss: float = MAX_LOSS - if trade_count >= self.config["hyperopt_min_trades"]: - loss = self.calculate_loss( - results=backtesting_results["results"], - trade_count=trade_count, - min_date=min_date, - max_date=max_date, - config=self.config, - processed=processed, - backtest_stats=strat_stats, - ) - return { - "loss": loss, - "params_dict": params_dict, - "params_details": params_details, - "params_not_optimized": not_optimized, - "results_metrics": strat_stats, - "results_explanation": results_explanation, - "total_profit": total_profit, - } - - def get_optimizer(self, dimensions: list[Dimension], cpu_count) -> Optimizer: - estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions) - - acq_optimizer = "sampling" - if isinstance(estimator, str): - if estimator not in ("GP", "RF", "ET", "GBRT"): - raise OperationalException(f"Estimator {estimator} not supported.") - else: - acq_optimizer = "auto" - - logger.info(f"Using estimator {estimator}.") - return Optimizer( - dimensions, - base_estimator=estimator, - acq_optimizer=acq_optimizer, - n_initial_points=INITIAL_POINTS, - acq_optimizer_kwargs={"n_jobs": cpu_count}, - random_state=self.random_state, - model_queue_size=SKOPT_MODEL_QUEUE_SIZE, - ) - def run_optimizer_parallel(self, parallel: Parallel, asked: list[list]) -> list[dict[str, Any]]: """Start optimizer in a parallel way""" return parallel( - delayed(wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked + delayed(wrap_non_picklable_objects(self.hyperopter.generate_optimizer))(v) + for v in asked ) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 - def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]: - preprocessed = self.backtesting.strategy.advise_all_indicators(data) - - # Trim startup period from analyzed dataframe to get correct dates for output. - # This is only used to keep track of min/max date after trimming. - # The result is NOT returned from this method, actual trimming happens in backtesting. - trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) - self.min_date, self.max_date = get_timerange(trimmed) - if not self.market_change: - self.market_change = calculate_market_change(trimmed, "close") - - # Real trimming will happen as part of backtesting. - return preprocessed - - def prepare_hyperopt_data(self) -> None: - HyperoptStateContainer.set_state(HyperoptState.DATALOAD) - data, self.timerange = self.backtesting.load_bt_data() - self.backtesting.load_bt_data_detail() - logger.info("Dataload complete. Calculating indicators") - - if not self.analyze_per_epoch: - HyperoptStateContainer.set_state(HyperoptState.INDICATORS) - - preprocessed = self.advise_and_trim(data) - - logger.info( - f"Hyperopting with data from " - f"{self.min_date.strftime(DATETIME_PRINT_FORMAT)} " - f"up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} " - f"({(self.max_date - self.min_date).days} days).." - ) - # Store non-trimmed data - will be trimmed after signal generation. - dump(preprocessed, self.data_pickle_file) - else: - dump(data, self.data_pickle_file) - def get_asked_points(self, n_points: int) -> tuple[list[list[Any]], list[bool]]: """ Enforce points returned from `self.opt.ask` have not been already evaluated @@ -595,27 +251,16 @@ class Hyperopt: self.random_state = self._set_random_state(self.config.get("hyperopt_random_state")) logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 - # Initialize spaces ... - self.init_spaces() - - self.prepare_hyperopt_data() - - # We don't need exchange instance anymore while running hyperopt - self.backtesting.exchange.close() - self.backtesting.exchange._api = None - self.backtesting.exchange._api_async = None - self.backtesting.exchange.loop = None # type: ignore - self.backtesting.exchange._loop_lock = None # type: ignore - self.backtesting.exchange._cache_lock = None # type: ignore - # self.backtesting.exchange = None # type: ignore - self.backtesting.pairlists = None # type: ignore + self.hyperopter.prepare_hyperopt() cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get("hyperopt_jobs", -1) logger.info(f"Number of parallel jobs set as: {config_jobs}") - self.opt = self.get_optimizer(self.dimensions, config_jobs) + self.opt = self.hyperopter.get_optimizer( + config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE + ) try: with Parallel(n_jobs=config_jobs) as parallel: @@ -638,7 +283,7 @@ class Hyperopt: # First analysis not in parallel mode when using --analyze-per-epoch. # This allows dataprovider to load it's informative cache. asked, is_random = self.get_asked_points(n_points=1) - f_val0 = self.generate_optimizer(asked[0]) + f_val0 = self.hyperopter.generate_optimizer(asked[0]) self.opt.tell(asked, [f_val0["loss"]]) self.evaluate_result(f_val0, 1, is_random[0]) pbar.update(task, advance=1) @@ -672,7 +317,9 @@ class Hyperopt: if self.current_best_epoch: HyperoptTools.try_export_params( - self.config, self.backtesting.strategy.get_strategy_name(), self.current_best_epoch + self.config, + self.hyperopter.get_strategy_name(), + self.current_best_epoch, ) HyperoptTools.show_epoch_details( diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py new file mode 100644 index 000000000..4a17b0717 --- /dev/null +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -0,0 +1,443 @@ +# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement + +""" +This module contains the hyperopt logic +""" + +import logging +import sys +import warnings +from datetime import datetime, timezone +from typing import Any + +from joblib import dump, load +from joblib.externals import cloudpickle +from pandas import DataFrame + +from freqtrade.constants import DATETIME_PRINT_FORMAT, Config +from freqtrade.data.converter import trim_dataframes +from freqtrade.data.history import get_timerange +from freqtrade.data.metrics import calculate_market_change +from freqtrade.enums import HyperoptState +from freqtrade.exceptions import OperationalException +from freqtrade.misc import deep_merge_dicts +from freqtrade.optimize.backtesting import Backtesting + +# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto +from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss +from freqtrade.optimize.hyperopt_tools import ( + HyperoptStateContainer, + HyperoptTools, +) +from freqtrade.optimize.optimize_reports import generate_strategy_stats +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver + + +# Suppress scikit-learn FutureWarnings from skopt +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + from skopt import Optimizer + from skopt.space import Dimension + +logger = logging.getLogger(__name__) + + +MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization + + +class HyperOptimizer: + """ + HyperoptOptimizer class + This class is sent to the hyperopt worker processes. + """ + + def __init__(self, config: Config) -> None: + self.buy_space: list[Dimension] = [] + self.sell_space: list[Dimension] = [] + self.protection_space: list[Dimension] = [] + self.roi_space: list[Dimension] = [] + self.stoploss_space: list[Dimension] = [] + self.trailing_space: list[Dimension] = [] + self.max_open_trades_space: list[Dimension] = [] + self.dimensions: list[Dimension] = [] + + self.config = config + self.min_date: datetime + self.max_date: datetime + + self.backtesting = Backtesting(self.config) + self.pairlist = self.backtesting.pairlists.whitelist + self.custom_hyperopt: HyperOptAuto + self.analyze_per_epoch = self.config.get("analyze_per_epoch", False) + + if not self.config.get("hyperopt"): + self.custom_hyperopt = HyperOptAuto(self.config) + else: + raise OperationalException( + "Using separate Hyperopt files has been removed in 2021.9. Please convert " + "your existing Hyperopt file to the new Hyperoptable strategy interface" + ) + + self.backtesting._set_strategy(self.backtesting.strategylist[0]) + self.custom_hyperopt.strategy = self.backtesting.strategy + + self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) + self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( + self.config + ) + self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function + + self.data_pickle_file = ( + self.config["user_data_dir"] / "hyperopt_results" / "hyperopt_tickerdata.pkl" + ) + + self.market_change = 0.0 + + if HyperoptTools.has_space(self.config, "sell"): + # Make sure use_exit_signal is enabled + self.config["use_exit_signal"] = True + + def prepare_hyperopt(self) -> None: + # Initialize spaces ... + self.init_spaces() + + self.prepare_hyperopt_data() + + # We don't need exchange instance anymore while running hyperopt + self.backtesting.exchange.close() + self.backtesting.exchange._api = None + self.backtesting.exchange._api_async = None + self.backtesting.exchange.loop = None # type: ignore + self.backtesting.exchange._loop_lock = None # type: ignore + self.backtesting.exchange._cache_lock = None # type: ignore + # self.backtesting.exchange = None # type: ignore + self.backtesting.pairlists = None # type: ignore + + def get_strategy_name(self) -> str: + return self.backtesting.strategy.get_strategy_name() + + def hyperopt_pickle_magic(self, bases) -> None: + """ + Hyperopt magic to allow strategy inheritance across files. + For this to properly work, we need to register the module of the imported class + to pickle as value. + """ + for modules in bases: + if modules.__name__ != "IStrategy": + cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) + self.hyperopt_pickle_magic(modules.__bases__) + + def _get_params_dict( + self, dimensions: list[Dimension], raw_params: list[Any] + ) -> dict[str, Any]: + # Ensure the number of dimensions match + # the number of parameters in the list. + if len(raw_params) != len(dimensions): + raise ValueError("Mismatch in number of search-space dimensions.") + + # Return a dict where the keys are the names of the dimensions + # and the values are taken from the list of parameters. + return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} + + def get_optimizer( + self, + cpu_count: int, + random_state: int, + initial_points: int, + model_queue_size: int, + ) -> Optimizer: + dimensions = self.dimensions + estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions) + + acq_optimizer = "sampling" + if isinstance(estimator, str): + if estimator not in ("GP", "RF", "ET", "GBRT"): + raise OperationalException(f"Estimator {estimator} not supported.") + else: + acq_optimizer = "auto" + + logger.info(f"Using estimator {estimator}.") + return Optimizer( + dimensions, + base_estimator=estimator, + acq_optimizer=acq_optimizer, + n_initial_points=initial_points, + acq_optimizer_kwargs={"n_jobs": cpu_count}, + random_state=random_state, + model_queue_size=model_queue_size, + ) + + def _get_params_details(self, params: dict) -> dict: + """ + Return the params for each space + """ + result: dict = {} + + if HyperoptTools.has_space(self.config, "buy"): + result["buy"] = {p.name: params.get(p.name) for p in self.buy_space} + if HyperoptTools.has_space(self.config, "sell"): + result["sell"] = {p.name: params.get(p.name) for p in self.sell_space} + if HyperoptTools.has_space(self.config, "protection"): + result["protection"] = {p.name: params.get(p.name) for p in self.protection_space} + if HyperoptTools.has_space(self.config, "roi"): + result["roi"] = { + str(k): v for k, v in self.custom_hyperopt.generate_roi_table(params).items() + } + if HyperoptTools.has_space(self.config, "stoploss"): + result["stoploss"] = {p.name: params.get(p.name) for p in self.stoploss_space} + if HyperoptTools.has_space(self.config, "trailing"): + result["trailing"] = self.custom_hyperopt.generate_trailing_params(params) + if HyperoptTools.has_space(self.config, "trades"): + result["max_open_trades"] = { + "max_open_trades": ( + self.backtesting.strategy.max_open_trades + if self.backtesting.strategy.max_open_trades != float("inf") + else -1 + ) + } + + return result + + def _get_no_optimize_details(self) -> dict[str, Any]: + """ + Get non-optimized parameters + """ + result: dict[str, Any] = {} + strategy = self.backtesting.strategy + if not HyperoptTools.has_space(self.config, "roi"): + result["roi"] = {str(k): v for k, v in strategy.minimal_roi.items()} + if not HyperoptTools.has_space(self.config, "stoploss"): + result["stoploss"] = {"stoploss": strategy.stoploss} + if not HyperoptTools.has_space(self.config, "trailing"): + result["trailing"] = { + "trailing_stop": strategy.trailing_stop, + "trailing_stop_positive": strategy.trailing_stop_positive, + "trailing_stop_positive_offset": strategy.trailing_stop_positive_offset, + "trailing_only_offset_is_reached": strategy.trailing_only_offset_is_reached, + } + if not HyperoptTools.has_space(self.config, "trades"): + result["max_open_trades"] = {"max_open_trades": strategy.max_open_trades} + return result + + def init_spaces(self): + """ + 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() + + 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() + + 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 = ( + self.buy_space + + self.sell_space + + self.protection_space + + self.roi_space + + self.stoploss_space + + self.trailing_space + + self.max_open_trades_space + ) + + 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] + + def generate_optimizer(self, raw_params: list[Any]) -> dict[str, Any]: + """ + Used Optimize function. + Called once per epoch to optimize whatever is configured. + Keep this function as optimized as possible! + """ + HyperoptStateContainer.set_state(HyperoptState.OPTIMIZE) + backtest_start_time = datetime.now(timezone.utc) + params_dict = self._get_params_dict(self.dimensions, raw_params) + + # 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") + + if HyperoptTools.has_space(self.config, "roi"): + self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table( + params_dict + ) + + if HyperoptTools.has_space(self.config, "stoploss"): + self.backtesting.strategy.stoploss = params_dict["stoploss"] + + if HyperoptTools.has_space(self.config, "trailing"): + d = self.custom_hyperopt.generate_trailing_params(params_dict) + self.backtesting.strategy.trailing_stop = d["trailing_stop"] + self.backtesting.strategy.trailing_stop_positive = d["trailing_stop_positive"] + self.backtesting.strategy.trailing_stop_positive_offset = d[ + "trailing_stop_positive_offset" + ] + self.backtesting.strategy.trailing_only_offset_is_reached = d[ + "trailing_only_offset_is_reached" + ] + + if HyperoptTools.has_space(self.config, "trades"): + if self.config["stake_amount"] == "unlimited" and ( + params_dict["max_open_trades"] == -1 or params_dict["max_open_trades"] == 0 + ): + # Ignore unlimited max open trades if stake amount is unlimited + params_dict.update({"max_open_trades": self.config["max_open_trades"]}) + + updated_max_open_trades = ( + int(params_dict["max_open_trades"]) + if (params_dict["max_open_trades"] != -1 and params_dict["max_open_trades"] != 0) + else float("inf") + ) + + self.config.update({"max_open_trades": updated_max_open_trades}) + + self.backtesting.strategy.max_open_trades = updated_max_open_trades + + with self.data_pickle_file.open("rb") as f: + processed = load(f, mmap_mode="r") + if self.analyze_per_epoch: + # Data is not yet analyzed, rerun populate_indicators. + processed = self.advise_and_trim(processed) + + bt_results = self.backtesting.backtest( + processed=processed, start_date=self.min_date, end_date=self.max_date + ) + backtest_end_time = datetime.now(timezone.utc) + bt_results.update( + { + "backtest_start_time": int(backtest_start_time.timestamp()), + "backtest_end_time": int(backtest_end_time.timestamp()), + } + ) + + return self._get_results_dict( + bt_results, self.min_date, self.max_date, params_dict, processed=processed + ) + + def _get_results_dict( + self, + backtesting_results: dict[str, Any], + min_date: datetime, + max_date: datetime, + params_dict: dict[str, Any], + processed: dict[str, DataFrame], + ) -> dict[str, Any]: + params_details = self._get_params_details(params_dict) + + strat_stats = generate_strategy_stats( + self.pairlist, + self.backtesting.strategy.get_strategy_name(), + backtesting_results, + min_date, + max_date, + market_change=self.market_change, + is_hyperopt=True, + ) + results_explanation = HyperoptTools.format_results_explanation_string( + strat_stats, self.config["stake_currency"] + ) + + not_optimized = self.backtesting.strategy.get_no_optimize_params() + not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details()) + + trade_count = strat_stats["total_trades"] + total_profit = strat_stats["profit_total"] + + # If this evaluation contains too short amount of trades to be + # interesting -- consider it as 'bad' (assigned max. loss value) + # in order to cast this hyperspace point away from optimization + # path. We do not want to optimize 'hodl' strategies. + loss: float = MAX_LOSS + if trade_count >= self.config["hyperopt_min_trades"]: + loss = self.calculate_loss( + results=backtesting_results["results"], + trade_count=trade_count, + min_date=min_date, + max_date=max_date, + config=self.config, + processed=processed, + backtest_stats=strat_stats, + ) + return { + "loss": loss, + "params_dict": params_dict, + "params_details": params_details, + "params_not_optimized": not_optimized, + "results_metrics": strat_stats, + "results_explanation": results_explanation, + "total_profit": total_profit, + } + + def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]: + preprocessed = self.backtesting.strategy.advise_all_indicators(data) + + # Trim startup period from analyzed dataframe to get correct dates for output. + # This is only used to keep track of min/max date after trimming. + # The result is NOT returned from this method, actual trimming happens in backtesting. + trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) + self.min_date, self.max_date = get_timerange(trimmed) + if not self.market_change: + self.market_change = calculate_market_change(trimmed, "close") + + # Real trimming will happen as part of backtesting. + return preprocessed + + def prepare_hyperopt_data(self) -> None: + HyperoptStateContainer.set_state(HyperoptState.DATALOAD) + data, self.timerange = self.backtesting.load_bt_data() + self.backtesting.load_bt_data_detail() + logger.info("Dataload complete. Calculating indicators") + + if not self.analyze_per_epoch: + HyperoptStateContainer.set_state(HyperoptState.INDICATORS) + + preprocessed = self.advise_and_trim(data) + + logger.info( + f"Hyperopting with data from " + f"{self.min_date.strftime(DATETIME_PRINT_FORMAT)} " + f"up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} " + f"({(self.max_date - self.min_date).days} days).." + ) + # Store non-trimmed data - will be trimmed after signal generation. + dump(preprocessed, self.data_pickle_file) + else: + dump(data, self.data_pickle_file) From b6d1f9f754da61f94c8ae69f3c80d75dcda48401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 15:44:16 +0100 Subject: [PATCH 07/15] test: update hyperopt tests for new structure --- tests/optimize/test_hyperopt.py | 293 ++++++++++++++++++-------------- 1 file changed, 165 insertions(+), 128 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f7b7c93b7..a0cc4eb73 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -222,7 +222,7 @@ def test_start_no_data(mocker, hyperopt_conf, tmp_path) -> None: patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch("freqtrade.data.history.load_pair_history", MagicMock(return_value=pd.DataFrame)) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -315,12 +315,17 @@ def test_roi_table_generation(hyperopt) -> None: "roi_p3": 3, } - assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} + assert hyperopt.hyperopter.custom_hyperopt.generate_roi_table(params) == { + 0: 6, + 15: 3, + 25: 1, + 30: 0, + } def test_params_no_optimize_details(hyperopt) -> None: - hyperopt.config["spaces"] = ["buy"] - res = hyperopt._get_no_optimize_details() + hyperopt.hyperopter.config["spaces"] = ["buy"] + res = hyperopt.hyperopter._get_no_optimize_details() assert isinstance(res, dict) assert "trailing" in res assert res["trailing"]["trailing_stop"] is False @@ -333,9 +338,11 @@ def test_params_no_optimize_details(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") mocker.patch( @@ -343,7 +350,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: MagicMock(return_value=(MagicMock(), None)), ) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) # Dummy-reduce points to ensure scikit-learn is forced to generate new values @@ -367,8 +374,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: del hyperopt_conf["timeframe"] hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -379,10 +386,12 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: # Should be called for historical candle data assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] - assert hasattr(hyperopt.backtesting, "_position_stacking") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_entry") + assert ( + hyperopt.hyperopter.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] + ) + assert hasattr(hyperopt.hyperopter.backtesting, "_position_stacking") def test_hyperopt_format_results(hyperopt): @@ -461,7 +470,7 @@ def test_hyperopt_format_results(hyperopt): def test_populate_indicators(hyperopt, testdatadir) -> None: data = load_data(testdatadir, "1m", ["UNITTEST/BTC"], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) + dataframes = hyperopt.hyperopter.backtesting.strategy.advise_all_indicators(data) dataframe = dataframes["UNITTEST/BTC"] # Check if some indicators are generated. We will not test all of them @@ -522,16 +531,19 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: } mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.Backtesting.backtest", return_value=backtest_result + "freqtrade.optimize.hyperopt.hyperopt_optimizer.Backtesting.backtest", + return_value=backtest_result, ) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", return_value=(dt_utc(2017, 12, 10), dt_utc(2017, 12, 13)), ) patch_exchange(mocker) mocker.patch.object(Path, "open") mocker.patch("freqtrade.configuration.config_validation.validate_config_schema") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.load", return_value={"XRP/BTC": None}) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.load", return_value={"XRP/BTC": None} + ) optimizer_param = { "buy_plusdi": 0.02, @@ -591,10 +603,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: } hyperopt = Hyperopt(hyperopt_conf) - hyperopt.min_date = dt_utc(2017, 12, 10) - hyperopt.max_date = dt_utc(2017, 12, 13) - hyperopt.init_spaces() - generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) + hyperopt.hyperopter.min_date = dt_utc(2017, 12, 10) + hyperopt.hyperopter.max_date = dt_utc(2017, 12, 13) + hyperopt.hyperopter.init_spaces() + generate_optimizer_value = hyperopt.hyperopter.generate_optimizer( + list(optimizer_param.values()) + ) assert generate_optimizer_value == response_expected @@ -614,17 +628,19 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) mocker.patch( "freqtrade.optimize.backtesting.Backtesting.load_bt_data", MagicMock(return_value=(MagicMock(), None)), ) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -660,8 +676,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -679,16 +695,18 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") mocker.patch("freqtrade.optimize.hyperopt.hyperopt.file_dump_json") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) mocker.patch( "freqtrade.optimize.backtesting.Backtesting.load_bt_data", MagicMock(return_value=(MagicMock(), None)), ) mocker.patch( - "freqtrade.optimize.hyperopt.hyperopt.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -716,8 +734,8 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: hyperopt_conf.update({"print_json": True}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -734,16 +752,18 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -772,8 +792,8 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -787,16 +807,18 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -818,8 +840,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non hyperopt_conf.update({"spaces": "roi stoploss"}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -830,21 +852,23 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] - assert hasattr(hyperopt.backtesting, "_position_stacking") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_entry") + assert ( + hyperopt.hyperopter.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] + ) + assert hasattr(hyperopt.hyperopter.backtesting, "_position_stacking") def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump", MagicMock()) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -862,30 +886,32 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None: ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + 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 *"): - hyperopt.init_spaces() + hyperopt.hyperopter.init_spaces() hyperopt.config["hyperopt_ignore_missing_space"] = True caplog.clear() - hyperopt.init_spaces() + hyperopt.hyperopter.init_spaces() assert log_has_re(r"The 'protection' space is included into *", caplog) - assert hyperopt.protection_space == [] + assert hyperopt.hyperopter.protection_space == [] def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -907,8 +933,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: hyperopt_conf.update({"spaces": "buy"}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -919,23 +945,27 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] - assert hasattr(hyperopt.backtesting, "_position_stacking") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_entry") + assert ( + hyperopt.hyperopter.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] + ) + assert hasattr(hyperopt.hyperopter.backtesting, "_position_stacking") def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: - dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump") + dumper = mocker.patch("freqtrade.optimize.hyperopt.hyperopt_optimizer.dump") dumper2 = mocker.patch("freqtrade.optimize.hyperopt.Hyperopt._save_result") - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.calculate_market_change", return_value=1.5) + mocker.patch( + "freqtrade.optimize.hyperopt.hyperopt_optimizer.calculate_market_change", return_value=1.5 + ) 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) @@ -961,8 +991,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) hyperopt.start() @@ -973,10 +1003,12 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] - assert hasattr(hyperopt.backtesting, "_position_stacking") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.hyperopter.backtesting.strategy, "advise_entry") + assert ( + hyperopt.hyperopter.backtesting.strategy.max_open_trades == hyperopt_conf["max_open_trades"] + ) + assert hasattr(hyperopt.hyperopter.backtesting, "_position_stacking") @pytest.mark.parametrize( @@ -988,14 +1020,14 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: ], ) def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: - mocker.patch("freqtrade.optimize.hyperopt.hyperopt.dump", MagicMock()) + 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.get_timerange", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.get_timerange", MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))), ) mocker.patch( @@ -1008,8 +1040,8 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: hyperopt_conf.update({"spaces": space}) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() - hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + hyperopt.hyperopter.backtesting.strategy.advise_all_indicators = MagicMock() + hyperopt.hyperopter.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"): hyperopt.start() @@ -1031,32 +1063,33 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None } ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) - assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_started is True - assert hyperopt.backtesting.strategy.bot_loop_started is False + opt = hyperopt.hyperopter + opt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) + assert isinstance(opt.custom_hyperopt, HyperOptAuto) + assert isinstance(opt.backtesting.strategy.buy_rsi, IntParameter) + assert opt.backtesting.strategy.bot_started is True + assert opt.backtesting.strategy.bot_loop_started is False - assert hyperopt.backtesting.strategy.buy_rsi.in_space is True - assert hyperopt.backtesting.strategy.buy_rsi.value == 35 - assert hyperopt.backtesting.strategy.sell_rsi.value == 74 - assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 - assert hyperopt.backtesting.strategy.max_open_trades == 1 - buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert opt.backtesting.strategy.buy_rsi.in_space is True + assert opt.backtesting.strategy.buy_rsi.value == 35 + assert opt.backtesting.strategy.sell_rsi.value == 74 + assert opt.backtesting.strategy.protection_cooldown_lookback.value == 30 + assert opt.backtesting.strategy.max_open_trades == 1 + buy_rsi_range = opt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) # Range from 0 - 50 (inclusive) assert len(list(buy_rsi_range)) == 51 hyperopt.start() # All values should've changed. - assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30 - assert hyperopt.backtesting.strategy.buy_rsi.value != 35 - assert hyperopt.backtesting.strategy.sell_rsi.value != 74 - assert hyperopt.backtesting.strategy.max_open_trades != 1 + assert opt.backtesting.strategy.protection_cooldown_lookback.value != 30 + assert opt.backtesting.strategy.buy_rsi.value != 35 + assert opt.backtesting.strategy.sell_rsi.value != 74 + assert opt.backtesting.strategy.max_open_trades != 1 - hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1" + opt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: "ET1" with pytest.raises(OperationalException, match="Estimator ET1 not supported."): - hyperopt.get_optimizer([], 2) + opt.get_optimizer(2, 42, 2, 2) @pytest.mark.filterwarnings("ignore::DeprecationWarning") @@ -1082,21 +1115,22 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path } ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 - hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 0.00001 - hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0 - hyperopt.backtesting.exchange._markets = get_markets() + opt = hyperopt.hyperopter + opt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + opt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 0.00001 + opt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0 + opt.backtesting.exchange._markets = get_markets() - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) - assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_started is True - assert hyperopt.backtesting.strategy.bot_loop_started is False + assert isinstance(opt.custom_hyperopt, HyperOptAuto) + assert isinstance(opt.backtesting.strategy.buy_rsi, IntParameter) + assert opt.backtesting.strategy.bot_started is True + assert opt.backtesting.strategy.bot_loop_started is False - assert hyperopt.backtesting.strategy.buy_rsi.in_space is True - assert hyperopt.backtesting.strategy.buy_rsi.value == 35 - assert hyperopt.backtesting.strategy.sell_rsi.value == 74 - assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 - buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert opt.backtesting.strategy.buy_rsi.in_space is True + assert opt.backtesting.strategy.buy_rsi.value == 35 + assert opt.backtesting.strategy.sell_rsi.value == 74 + assert opt.backtesting.strategy.protection_cooldown_lookback.value == 30 + buy_rsi_range = opt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) # Range from 0 - 50 (inclusive) assert len(list(buy_rsi_range)) == 51 @@ -1120,7 +1154,7 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fe } ) go = mocker.patch( - "freqtrade.optimize.hyperopt.Hyperopt.generate_optimizer", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer.generate_optimizer", return_value={ "loss": 0.05, "results_explanation": "foo result", @@ -1129,17 +1163,18 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fe }, ) hyperopt = Hyperopt(hyperopt_conf) - hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) - assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is False - assert hyperopt.backtesting.strategy.bot_started is True + opt = hyperopt.hyperopter + opt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) + assert isinstance(opt.custom_hyperopt, HyperOptAuto) + assert isinstance(opt.backtesting.strategy.buy_rsi, IntParameter) + assert opt.backtesting.strategy.bot_loop_started is False + assert opt.backtesting.strategy.bot_started is True - assert hyperopt.backtesting.strategy.buy_rsi.in_space is True - assert hyperopt.backtesting.strategy.buy_rsi.value == 35 - assert hyperopt.backtesting.strategy.sell_rsi.value == 74 - assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 - buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert opt.backtesting.strategy.buy_rsi.in_space is True + assert opt.backtesting.strategy.buy_rsi.value == 35 + assert opt.backtesting.strategy.sell_rsi.value == 74 + assert opt.backtesting.strategy.protection_cooldown_lookback.value == 30 + buy_rsi_range = opt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) # Range from 0 - 50 (inclusive) assert len(list(buy_rsi_range)) == 51 @@ -1183,17 +1218,17 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, ) hyperopt = Hyperopt(hyperopt_conf) mocker.patch( - "freqtrade.optimize.hyperopt.Hyperopt._get_params_dict", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", return_value={"max_open_trades": -1}, ) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) - assert hyperopt.backtesting.strategy.max_open_trades == 1 + assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 1 hyperopt.start() - assert hyperopt.backtesting.strategy.max_open_trades == 1 + assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 1 def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> None: @@ -1212,11 +1247,11 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N ) hyperopt = Hyperopt(hyperopt_conf) mocker.patch( - "freqtrade.optimize.hyperopt.Hyperopt._get_params_dict", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", return_value={"max_open_trades": -1}, ) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) hyperopt.start() @@ -1231,11 +1266,11 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> N hyperopt = Hyperopt(hyperopt_conf) mocker.patch( - "freqtrade.optimize.hyperopt.Hyperopt._get_params_dict", + "freqtrade.optimize.hyperopt.hyperopt_optimizer.HyperOptimizer._get_params_dict", return_value={"max_open_trades": -1}, ) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) hyperopt.start() @@ -1266,9 +1301,9 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No ) hyperopt = Hyperopt(hyperopt_conf) - assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.hyperopter.custom_hyperopt, HyperOptAuto) - hyperopt.custom_hyperopt.max_open_trades_space = lambda: [ + hyperopt.hyperopter.custom_hyperopt.max_open_trades_space = lambda: [ Integer(1, 10, name="max_open_trades") ] @@ -1286,11 +1321,13 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> No return wrapper - hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor( - hyperopt.backtesting.wallets._calculate_unlimited_stake_amount + hyperopt.hyperopter.backtesting.wallets._calculate_unlimited_stake_amount = ( + stake_amount_interceptor( + hyperopt.hyperopter.backtesting.wallets._calculate_unlimited_stake_amount + ) ) hyperopt.start() - assert hyperopt.backtesting.strategy.max_open_trades == 8 + assert hyperopt.hyperopter.backtesting.strategy.max_open_trades == 8 assert hyperopt.config["max_open_trades"] == 8 From 365c454da1f3c62f94d0eba1160d196376b2aab6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 15:50:24 +0100 Subject: [PATCH 08/15] chore: Improve import comments --- freqtrade/optimize/hyperopt/hyperopt.py | 4 ---- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 7 ++----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 61cb4e8f3..261e9962e 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -21,12 +21,8 @@ from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural - -# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules -from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput -from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss from freqtrade.optimize.hyperopt_tools import ( HyperoptStateContainer, HyperoptTools, diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 4a17b0717..d80ce0294 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -23,13 +23,10 @@ from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.backtesting import Backtesting -# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +# Import IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_loss.hyperopt_loss_interface import IHyperOptLoss -from freqtrade.optimize.hyperopt_tools import ( - HyperoptStateContainer, - HyperoptTools, -) +from freqtrade.optimize.hyperopt_tools import HyperoptStateContainer, HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver From 119b73ead2f95ca9a9ce12ca4bc8801d08f9c85d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Nov 2024 15:55:26 +0100 Subject: [PATCH 09/15] chore: improtve method sorting --- .../optimize/hyperopt/hyperopt_optimizer.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index d80ce0294..59b352bdd 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -137,34 +137,6 @@ class HyperOptimizer: # and the values are taken from the list of parameters. return {d.name: v for d, v in zip(dimensions, raw_params, strict=False)} - def get_optimizer( - self, - cpu_count: int, - random_state: int, - initial_points: int, - model_queue_size: int, - ) -> Optimizer: - dimensions = self.dimensions - estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions) - - acq_optimizer = "sampling" - if isinstance(estimator, str): - if estimator not in ("GP", "RF", "ET", "GBRT"): - raise OperationalException(f"Estimator {estimator} not supported.") - else: - acq_optimizer = "auto" - - logger.info(f"Using estimator {estimator}.") - return Optimizer( - dimensions, - base_estimator=estimator, - acq_optimizer=acq_optimizer, - n_initial_points=initial_points, - acq_optimizer_kwargs={"n_jobs": cpu_count}, - random_state=random_state, - model_queue_size=model_queue_size, - ) - def _get_params_details(self, params: dict) -> dict: """ Return the params for each space @@ -403,6 +375,34 @@ class HyperOptimizer: "total_profit": total_profit, } + def get_optimizer( + self, + cpu_count: int, + random_state: int, + initial_points: int, + model_queue_size: int, + ) -> Optimizer: + dimensions = self.dimensions + estimator = self.custom_hyperopt.generate_estimator(dimensions=dimensions) + + acq_optimizer = "sampling" + if isinstance(estimator, str): + if estimator not in ("GP", "RF", "ET", "GBRT"): + raise OperationalException(f"Estimator {estimator} not supported.") + else: + acq_optimizer = "auto" + + logger.info(f"Using estimator {estimator}.") + return Optimizer( + dimensions, + base_estimator=estimator, + acq_optimizer=acq_optimizer, + n_initial_points=initial_points, + acq_optimizer_kwargs={"n_jobs": cpu_count}, + random_state=random_state, + model_queue_size=model_queue_size, + ) + def advise_and_trim(self, data: dict[str, DataFrame]) -> dict[str, DataFrame]: preprocessed = self.backtesting.strategy.advise_all_indicators(data) From 7e96e7af83189e216250953b344155f3224d2f33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 06:25:12 +0100 Subject: [PATCH 10/15] feat: add hyperopt log handlers to allow for multiprocessing logging --- freqtrade/optimize/hyperopt/hyperopt.py | 28 +++++++++--- .../optimize/hyperopt/hyperopt_logger.py | 43 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 freqtrade/optimize/hyperopt/hyperopt_logger.py diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 261e9962e..1fd3bf2ba 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -9,6 +9,7 @@ import random import sys from datetime import datetime from math import ceil +from multiprocessing import Manager from pathlib import Path from typing import Any @@ -21,6 +22,7 @@ from freqtrade.constants import FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.enums import HyperoptState from freqtrade.exceptions import OperationalException from freqtrade.misc import file_dump_json, plural +from freqtrade.optimize.hyperopt.hyperopt_logger import logging_mp_handle, logging_mp_setup from freqtrade.optimize.hyperopt.hyperopt_optimizer import HyperOptimizer from freqtrade.optimize.hyperopt.hyperopt_output import HyperoptOutput from freqtrade.optimize.hyperopt_tools import ( @@ -162,10 +164,16 @@ class Hyperopt: def run_optimizer_parallel(self, parallel: Parallel, asked: list[list]) -> list[dict[str, Any]]: """Start optimizer in a parallel way""" - return parallel( - delayed(wrap_non_picklable_objects(self.hyperopter.generate_optimizer))(v) - for v in asked - ) + + def optimizer_wrapper(*args, **kwargs): + # global log queue. This must happen in the file that initializes Parallel + logging_mp_setup( + log_queue, logging.INFO if self.config["verbosity"] < 1 else logging.DEBUG + ) + + return self.hyperopter.generate_optimizer(*args, **kwargs) + + return parallel(delayed(wrap_non_picklable_objects(optimizer_wrapper))(v) for v in asked) def _set_random_state(self, random_state: int | None) -> int: return random_state or random.randint(1, 2**16 - 1) # noqa: S311 @@ -243,6 +251,15 @@ class Hyperopt: self._save_result(val) + def _setup_logging_mp_workaround(self) -> None: + """ + Workaround for logging in child processes. + local_queue must be a global in the file that initializes Parallel. + """ + global log_queue + m = Manager() + log_queue = m.Queue() + def start(self) -> None: self.random_state = self._set_random_state(self.config.get("hyperopt_random_state")) logger.info(f"Using optimizer random state: {self.random_state}") @@ -257,7 +274,7 @@ class Hyperopt: self.opt = self.hyperopter.get_optimizer( config_jobs, self.random_state, INITIAL_POINTS, SKOPT_MODEL_QUEUE_SIZE ) - + self._setup_logging_mp_workaround() try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() @@ -302,6 +319,7 @@ class Hyperopt: self.evaluate_result(val, current, is_random[j]) pbar.update(task, advance=1) + logging_mp_handle(log_queue) except KeyboardInterrupt: print("User interrupted..") diff --git a/freqtrade/optimize/hyperopt/hyperopt_logger.py b/freqtrade/optimize/hyperopt/hyperopt_logger.py new file mode 100644 index 000000000..e0b55b45d --- /dev/null +++ b/freqtrade/optimize/hyperopt/hyperopt_logger.py @@ -0,0 +1,43 @@ +import logging +from logging.handlers import QueueHandler +from multiprocessing import Queue, current_process +from queue import Empty + + +logger = logging.getLogger(__name__) + + +def logging_mp_setup(log_queue: Queue, verbosity: int): + """ + Setup logging in a child process. + Must be called in the child process before logging. + log_queue MUST be passed to the child process via inheritance + Which essentially means that the log_queue must be a global, created in the same + file as Parallel is initialized. + """ + current_proc = current_process().name + if current_proc != "MainProcess": + h = QueueHandler(log_queue) + root = logging.getLogger() + root.setLevel(verbosity) + root.addHandler(h) + + +def logging_mp_handle(q: Queue): + """ + Handle logging in a child process. + Must be called in the child process after logging. + """ + + try: + while True: + record = q.get(block=False) + # logger1 = logging.getLogger(record.name) + if record is None: + break + logger.handle(record) + + except Empty: + logger.info("empty") + # print("empty") + pass From 67415dd7e227008f6943b8052ff07b9b44c7758d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 19:53:04 +0100 Subject: [PATCH 11/15] chore: improved module docstring --- freqtrade/optimize/hyperopt/hyperopt_optimizer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py index 59b352bdd..bd234aa57 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_optimizer.py +++ b/freqtrade/optimize/hyperopt/hyperopt_optimizer.py @@ -1,7 +1,6 @@ -# pragma pylint: disable=too-many-instance-attributes, pointless-string-statement - """ -This module contains the hyperopt logic +This module contains the hyperopt optimizer class, which needs to be pickled +and will be sent to the hyperopt worker processes. """ import logging From 81a622a9fa8e880f3b9d4a1a3e776d3d5439d590 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 19:53:14 +0100 Subject: [PATCH 12/15] chore: remove unnecessary log messages --- freqtrade/optimize/hyperopt/hyperopt_logger.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_logger.py b/freqtrade/optimize/hyperopt/hyperopt_logger.py index e0b55b45d..bb0c026b6 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_logger.py +++ b/freqtrade/optimize/hyperopt/hyperopt_logger.py @@ -32,12 +32,9 @@ def logging_mp_handle(q: Queue): try: while True: record = q.get(block=False) - # logger1 = logging.getLogger(record.name) if record is None: break logger.handle(record) except Empty: - logger.info("empty") - # print("empty") pass From f05f173d23030b1165badf408ab3268d8593cd9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 19:53:59 +0100 Subject: [PATCH 13/15] chore: improved docstring for logging_mp_handle --- freqtrade/optimize/hyperopt/hyperopt_logger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt/hyperopt_logger.py b/freqtrade/optimize/hyperopt/hyperopt_logger.py index bb0c026b6..d6940ee3a 100644 --- a/freqtrade/optimize/hyperopt/hyperopt_logger.py +++ b/freqtrade/optimize/hyperopt/hyperopt_logger.py @@ -25,8 +25,8 @@ def logging_mp_setup(log_queue: Queue, verbosity: int): def logging_mp_handle(q: Queue): """ - Handle logging in a child process. - Must be called in the child process after logging. + Handle logging from a child process. + Must be called in the parent process to handle log messages from the child process. """ try: From 33d8e67a87419a0443e0d934a336b94dee87092c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 20:12:31 +0100 Subject: [PATCH 14/15] docs: add note about logging --- docs/hyperopt.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 2b703ec6b..f36b1c6b3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -913,6 +913,31 @@ Your epochs should therefore be aligned to the possible values - or you should b After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. +## Output debug messages from your strategy + +If you want to output debug messages from your strategy, you can use the `logging` module. By default, Freqtrade will output all messages with a level of `INFO` or higher. + + +``` python +import logging + + +logger = logging.getLogger(__name__) + + +class MyAwesomeStrategy(IStrategy): + ... + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + logger.info("This is a debug message") + ... + +``` + +!!! Note "using print" + Messages printed via `print()` will not be shown in the hyperopt output unless parallelism is disabled (`-j 1`). + It is recommended to use the `logging` module instead. + ## Validate backtesting results Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. @@ -920,6 +945,7 @@ Once the optimized strategy has been implemented into your strategy, you should To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt for Backtesting. ### Why do my backtest results not match my hyperopt results? + Should results not match, check the following factors: * You may have added parameters to hyperopt in `populate_indicators()` where they will be calculated only once **for all epochs**. If you are, for example, trying to optimise multiple SMA timeperiod values, the hyperoptable timeperiod parameter should be placed in `populate_entry_trend()` which is calculated every epoch. See [Optimizing an indicator parameter](https://www.freqtrade.io/en/stable/hyperopt/#optimizing-an-indicator-parameter). From fbb64db3ae249440f364f82504b65ac9319909b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Nov 2024 20:14:37 +0100 Subject: [PATCH 15/15] define log_queue globally --- freqtrade/optimize/hyperopt/hyperopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/hyperopt/hyperopt.py b/freqtrade/optimize/hyperopt/hyperopt.py index 1fd3bf2ba..253691d4a 100644 --- a/freqtrade/optimize/hyperopt/hyperopt.py +++ b/freqtrade/optimize/hyperopt/hyperopt.py @@ -42,6 +42,8 @@ INITIAL_POINTS = 30 # in the skopt model queue, to optimize memory consumption SKOPT_MODEL_QUEUE_SIZE = 10 +log_queue: Any + class Hyperopt: """