Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Optimizer] properly handle multiple and single options #2249

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions octobot/strategy_optimizer/strategy_design_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class ConfigTypes(enum.Enum):
NUMBER = "number"
BOOLEAN = "boolean"
OPTIONS = "options"
MULTIPLE_OPTIONS = "multiple-options"
UNKNOWN = "unknown"


Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
48 changes: 37 additions & 11 deletions tests/unit_tests/strategy_optimizer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand All @@ -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
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading