From bd195968c05ae730a49c61d33036ba404dd74834 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 11 Nov 2024 14:16:58 +0100 Subject: [PATCH] [Optimizer] properly handle multiple and single options --- .../strategy_design_optimizer.py | 35 +++++++++++++- .../unit_tests/strategy_optimizer/__init__.py | 48 ++++++++++++++----- .../test_strategy_design_optimizer.py | 10 ++-- 3 files changed, 77 insertions(+), 16 deletions(-) diff --git a/octobot/strategy_optimizer/strategy_design_optimizer.py b/octobot/strategy_optimizer/strategy_design_optimizer.py index b6d6800bf..3700c521f 100644 --- a/octobot/strategy_optimizer/strategy_design_optimizer.py +++ b/octobot/strategy_optimizer/strategy_design_optimizer.py @@ -53,6 +53,7 @@ class ConfigTypes(enum.Enum): NUMBER = "number" BOOLEAN = "boolean" OPTIONS = "options" + MULTIPLE_OPTIONS = "multiple-options" UNKNOWN = "unknown" @@ -75,6 +76,7 @@ class StrategyDesignOptimizer: CONFIG_USER_INPUTS = "user_inputs" CONFIG_FILTER_SETTINGS = "filters_settings" CONFIG_VALUE = "value" + CONFIG_TYPE = "type" CONFIG_TENTACLE = "tentacle" CONFIG_NESTED_TENTACLE_SEPARATOR = "_------_" CONFIG_TYPE = "type" @@ -791,8 +793,13 @@ def _generate_possible_values(self, config_element): values = [] try: if config_element[self.CONFIG_ENABLED]: + if config_element[self.CONFIG_TYPE] is ConfigTypes.MULTIPLE_OPTIONS: + self._generate_possible_multiple_options_values( + possible_values=values, + config_element_values=config_element[self.CONFIG_VALUE][self.CONFIG_VALUE], + ) if config_element[self.CONFIG_TYPE] is ConfigTypes.OPTIONS: - values = [[value] for value in config_element[self.CONFIG_VALUE][self.CONFIG_VALUE]] + values = [value for value in config_element[self.CONFIG_VALUE][self.CONFIG_VALUE]] if config_element[self.CONFIG_TYPE] is ConfigTypes.BOOLEAN: values = config_element[self.CONFIG_VALUE][self.CONFIG_VALUE] if config_element[self.CONFIG_TYPE] is ConfigTypes.NUMBER: @@ -825,7 +832,28 @@ def _get_all_possible_values(self, start, stop, step): while current <= d_stop: yield return_type(current) current += d_step - + + def _generate_possible_multiple_options_values( + self, + possible_values: list, + config_element_values, + parent_values: list | None = None, + ): + if parent_values is None: + parent_values = [] + for combinable_value in config_element_values: + if combinable_value not in parent_values: + new_parent_values = parent_values + [combinable_value] + # sort to check uniqueness + new_parent_values.sort() + if new_parent_values not in possible_values: + possible_values.append(new_parent_values) + self._generate_possible_multiple_options_values( + possible_values, + config_element_values, + new_parent_values, + ) + @staticmethod def get_accurate_number_type(*values): return int if all(isinstance(e, int) for e in values) else float @@ -841,6 +869,9 @@ def _get_config_elements(self): } def _get_config_type(self, value): + config_type = value.get(self.CONFIG_TYPE) + if config_type: + return ConfigTypes(config_type) config_values = value[self.CONFIG_VALUE] if isinstance(config_values, list): if not config_values: diff --git a/tests/unit_tests/strategy_optimizer/__init__.py b/tests/unit_tests/strategy_optimizer/__init__.py index 5c9cc98b6..426da544e 100644 --- a/tests/unit_tests/strategy_optimizer/__init__.py +++ b/tests/unit_tests/strategy_optimizer/__init__.py @@ -16,6 +16,7 @@ import pytest_asyncio import octobot.strategy_optimizer as strategy_optimizer +import octobot.strategy_optimizer.strategy_design_optimizer as strategy_design_optimizer import octobot_trading.api as trading_api import octobot_tentacles_manager.api as tentacles_manager_api import octobot_commons.enums as commons_enums @@ -24,6 +25,7 @@ import tentacles.Evaluator.Strategies as Strategies import tentacles.Evaluator.TA as Evaluator +import tentacles.Evaluator.RealTime as RealTimeEvaluator import tentacles.Trading.Mode as Mode @@ -39,13 +41,14 @@ async def optimizer_inputs(): return tentacles_setup_config, trading_mode -def _get_optimizer_user_input_config(tentacle, user_input_name, enabled, value): +def _get_optimizer_user_input_config(tentacle, user_input_name, enabled, value, value_type=None): return { get_ui_identifier(tentacle, user_input_name): { "enabled": enabled, "tentacle": tentacle.get_name(), "user_input": user_input_name, - "value": value + "value": value, + "type": value_type } } @@ -84,6 +87,15 @@ def _get_mocked_ui_config(): ), _get_optimizer_user_input_config( Strategies.SimpleStrategyEvaluator, evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME, True, + [ + commons_enums.TimeFrames.FIVE_MINUTES.value, + commons_enums.TimeFrames.ONE_HOUR.value, + commons_enums.TimeFrames.ONE_DAY.value, + ], + value_type=strategy_design_optimizer.ConfigTypes.MULTIPLE_OPTIONS.value + ), + _get_optimizer_user_input_config( + RealTimeEvaluator.InstantFluctuationsEvaluator, commons_constants.CONFIG_TIME_FRAME, True, [ commons_enums.TimeFrames.FIVE_MINUTES.value, commons_enums.TimeFrames.ONE_HOUR.value, @@ -145,37 +157,51 @@ def _get_mocked_filters_settings(): } -def _get_mocked_run_ui(ui_name, tentacle, value): +def _get_mocked_run_ui( + ui_name, tentacle, value): return { strategy_optimizer.StrategyDesignOptimizer.CONFIG_USER_INPUT: ui_name, strategy_optimizer.StrategyDesignOptimizer.CONFIG_TENTACLE: [tentacle.get_name()], - strategy_optimizer.StrategyDesignOptimizer.CONFIG_VALUE: value + strategy_optimizer.StrategyDesignOptimizer.CONFIG_VALUE: value, } -def _get_mocked_run(period_length, constrained_risk, time_frame): +def _get_mocked_run(period_length, constrained_risk, time_frame, time_frames): return ( _get_mocked_run_ui("period_length", Evaluator.RSIMomentumEvaluator, period_length), _get_mocked_run_ui("constrained_risk", Evaluator.RSIMomentumEvaluator, constrained_risk), _get_mocked_run_ui(evaluators_constants.STRATEGIES_REQUIRED_TIME_FRAME, Strategies.SimpleStrategyEvaluator, - [time_frame.value]), + time_frames), + _get_mocked_run_ui(commons_constants.CONFIG_TIME_FRAME, + RealTimeEvaluator.InstantFluctuationsEvaluator, + time_frame.value), _get_mocked_run_ui(Strategies.SimpleStrategyEvaluator.RE_EVAL_TA_ON_RT_OR_SOCIAL, Strategies.SimpleStrategyEvaluator, False), ) MOCKED_RUNS = [ - _get_mocked_run(period_length_val, constrained_risk_val, time_frame_val) + _get_mocked_run(period_length_val, constrained_risk_val, + time_frame_val, time_frames_vals) for period_length_val in (5, 7, 9, 11, 13) for constrained_risk_val in (1, 2) for time_frame_val in (commons_enums.TimeFrames.FIVE_MINUTES, commons_enums.TimeFrames.ONE_HOUR, commons_enums.TimeFrames.ONE_DAY) + for time_frames_vals in ( + [commons_enums.TimeFrames.FIVE_MINUTES.value], + [commons_enums.TimeFrames.ONE_HOUR.value], + [commons_enums.TimeFrames.ONE_DAY.value], + [commons_enums.TimeFrames.ONE_HOUR.value, commons_enums.TimeFrames.FIVE_MINUTES.value], + [commons_enums.TimeFrames.ONE_DAY.value, commons_enums.TimeFrames.ONE_HOUR.value], + [commons_enums.TimeFrames.ONE_DAY.value, commons_enums.TimeFrames.FIVE_MINUTES.value], + [commons_enums.TimeFrames.ONE_DAY.value, + commons_enums.TimeFrames.ONE_HOUR.value, + commons_enums.TimeFrames.FIVE_MINUTES.value]) ] -EXPECTED_RUNS_FROM_MOCK = { - index + 6: val # index + 6 as the first 5 runs have been filtered - for index, val in enumerate(MOCKED_RUNS) -} +EXPECTED_RUNS_FROM_MOCK = {} +for index, val in enumerate(MOCKED_RUNS): + EXPECTED_RUNS_FROM_MOCK[index] = val \ No newline at end of file diff --git a/tests/unit_tests/strategy_optimizer/test_strategy_design_optimizer.py b/tests/unit_tests/strategy_optimizer/test_strategy_design_optimizer.py index c38c941a9..7caa37d2f 100644 --- a/tests/unit_tests/strategy_optimizer/test_strategy_design_optimizer.py +++ b/tests/unit_tests/strategy_optimizer/test_strategy_design_optimizer.py @@ -50,14 +50,18 @@ async def test_generate_and_save_strategy_optimizer_runs(optimizer_inputs): mock.patch.object(strategy_optimizer.StrategyDesignOptimizer, "shuffle_and_select_runs", mock.Mock( side_effect=lambda *args, **_: args[0] )) as shuffle_and_select_runs_mock: - assert await bot_module_api.generate_and_save_strategy_optimizer_runs( + generated_runs = await bot_module_api.generate_and_save_strategy_optimizer_runs( trading_mode, tentacles_setup_config, optimizer_settings - ) == EXPECTED_RUNS_FROM_MOCK + ) + expected_run_list = list(EXPECTED_RUNS_FROM_MOCK.values()) + assert len(expected_run_list) == len(generated_runs) + for run in generated_runs.values(): + assert run in expected_run_list create_identifier_mock.assert_called_once() shuffle_and_select_runs_mock.assert_called_once() - _save_run_schedule_mock.assert_awaited_once_with(EXPECTED_RUNS_FROM_MOCK) + _save_run_schedule_mock.assert_awaited_once_with(generated_runs) async def test_resume_unknown_mode(optimizer_inputs):