From 52d394b75bc10a942dbbb76ce2e95d6398aff64a Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 2 Oct 2025 03:51:23 -0500 Subject: [PATCH 1/6] Add --parameter option to backtest commands --- lean/commands/backtest.py | 11 ++++ lean/commands/cloud/backtest.py | 13 +++- lean/components/api/backtest_client.py | 12 +++- lean/components/cloud/cloud_runner.py | 9 ++- lean/models/utils.py | 13 ++++ tests/commands/cloud/test_backtest.py | 39 +++++++++++- tests/commands/test_backtest.py | 82 ++++++++++++++++++++++++++ 7 files changed, 168 insertions(+), 11 deletions(-) diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index 82f53efd..fdf3233f 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -282,6 +282,10 @@ def _migrate_csharp_csproj(project_dir: Path) -> None: is_flag=True, default=False, help="Use the local LEAN engine image instead of pulling the latest version") +@option("--parameter", + type=(str, str), + multiple=True, + help="Key-value pairs to pass as backtest parameters") def backtest(project: Path, output: Optional[Path], detach: bool, @@ -298,6 +302,7 @@ def backtest(project: Path, extra_config: Optional[Tuple[str, str]], extra_docker_config: Optional[str], no_update: bool, + parameter: List[Tuple[str, str]] = None, **kwargs) -> None: """Backtest a project locally using Docker. @@ -396,6 +401,12 @@ def backtest(project: Path, build_and_configure_modules(addon_module, cli_addon_modules, organization_id, lean_config, kwargs, logger, environment_name, container_module_version) + if parameter: + from lean.models.utils import parse_parameters + parameters = parse_parameters(parameter) + logger.info(f"Using parameters from command line: {parameters}") + lean_config["parameters"] = parameters + lean_runner = container.lean_runner lean_runner.run_lean(lean_config, environment_name, diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index 415d435b..d766ae2a 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import List, Optional, Tuple from click import command, argument, option from lean.click import LeanCommand from lean.container import container @@ -27,7 +27,11 @@ is_flag=True, default=False, help="Automatically open the results in the browser when the backtest is finished") -def backtest(project: str, name: Optional[str], push: bool, open_browser: bool) -> None: +@option("--parameter", + type=(str, str), + multiple=True, + help="Key-value pairs to pass as backtest parameters") +def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, parameter: List[Tuple[str, str]]) -> None: """Backtest a project in the cloud. PROJECT must be the name or id of the project to run a backtest for. @@ -54,8 +58,11 @@ def backtest(project: str, name: Optional[str], push: bool, open_browser: bool) if name is None: name = container.name_generator.generate_name() + from lean.models.utils import parse_parameters + parameters = parse_parameters(parameter) + cloud_runner = container.cloud_runner - finished_backtest = cloud_runner.run_backtest(cloud_project, name) + finished_backtest = cloud_runner.run_backtest(cloud_project, name, parameters) if finished_backtest.error is None and finished_backtest.stacktrace is None: logger.info(finished_backtest.get_statistics_table()) diff --git a/lean/components/api/backtest_client.py b/lean/components/api/backtest_client.py index 1f3abc6d..53a7a9ad 100644 --- a/lean/components/api/backtest_client.py +++ b/lean/components/api/backtest_client.py @@ -39,21 +39,27 @@ def get(self, project_id: int, backtest_id: str) -> QCBacktest: return QCBacktest(**data["backtest"]) - def create(self, project_id: int, compile_id: str, name: str) -> QCBacktest: + def create(self, project_id: int, compile_id: str, name: str, parameters: Dict[str, any] = None) -> QCBacktest: """Creates a new backtest. :param project_id: the id of the project to create a backtest for :param compile_id: the id of a compilation of the given project :param name: the name of the new backtest + :param parameters: optional key-value parameters for the backtest :return: the created backtest """ from lean import __version__ - data = self._api.post("backtests/create", { + payload = { "projectId": project_id, "compileId": compile_id, "backtestName": name, "requestSource": f"CLI {__version__}" - }) + } + + if parameters: + payload["parameters"] = parameters + + data = self._api.post("backtests/create", payload) return QCBacktest(**data["backtest"]) diff --git a/lean/components/cloud/cloud_runner.py b/lean/components/cloud/cloud_runner.py index 8aa939be..a90626b1 100644 --- a/lean/components/cloud/cloud_runner.py +++ b/lean/components/cloud/cloud_runner.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List +from typing import List, Dict from click import confirm @@ -75,17 +75,20 @@ def is_backtest_done(self, backtest_data: QCBacktest, delay: float = 10.0): self._logger.error(f"Error checking backtest completion status for ID {backtest_data.backtestId}: {e}") raise - def run_backtest(self, project: QCProject, name: str) -> QCBacktest: + def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, any] = None) -> QCBacktest: """Runs a backtest in the cloud. :param project: the project to backtest :param name: the name of the backtest + :param parameters: optional key-value parameters for the backtest :return: the completed backtest """ finished_compile = self.compile_project(project) - created_backtest = self._api_client.backtests.create(project.projectId, finished_compile.compileId, name) + created_backtest = self._api_client.backtests.create(project.projectId, finished_compile.compileId, name, parameters) self._logger.info(f"Started backtest named '{name}' for project '{project.name}'") + if parameters: + self._logger.info(f"Using parameters: {parameters}") self._logger.info(f"Backtest url: {created_backtest.get_url()}") try: diff --git a/lean/models/utils.py b/lean/models/utils.py index 7c868d01..41166cba 100644 --- a/lean/models/utils.py +++ b/lean/models/utils.py @@ -13,6 +13,7 @@ from enum import Enum from pathlib import Path +from typing import List, Tuple, Dict from lean.models.pydantic import WrappedBaseModel @@ -48,3 +49,15 @@ class LeanLibraryReference(WrappedBaseModel): """The information of a library reference in a project's config.json file""" name: str path: Path + +def parse_parameters(parameters: List[Tuple[str, str]]) -> Dict[str, any]: + """Parse parameters from command line to appropriate types.""" + parsed = {} + for key, value in parameters: + try: + num = float(value) + parsed_value = int(num) if num.is_integer() else num + except ValueError: + parsed_value = value + parsed[key] = parsed_value + return parsed diff --git a/tests/commands/cloud/test_backtest.py b/tests/commands/cloud/test_backtest.py index 7770c1ed..53268e3a 100644 --- a/tests/commands/cloud/test_backtest.py +++ b/tests/commands/cloud/test_backtest.py @@ -53,7 +53,7 @@ def test_cloud_backtest_runs_project_by_id() -> None: assert result.exit_code == 0 - cloud_runner.run_backtest.assert_called_once_with(project, mock.ANY) + cloud_runner.run_backtest.assert_called_once_with(project, mock.ANY, mock.ANY) def test_cloud_backtest_runs_project_by_name() -> None: @@ -73,7 +73,7 @@ def test_cloud_backtest_runs_project_by_name() -> None: assert result.exit_code == 0 - cloud_runner.run_backtest.assert_called_once_with(project, mock.ANY) + cloud_runner.run_backtest.assert_called_once_with(project, mock.ANY, mock.ANY) def test_cloud_backtest_uses_given_name() -> None: @@ -240,3 +240,38 @@ def test_cloud_backtest_aborts_when_input_matches_no_cloud_project() -> None: assert result.exit_code != 0 cloud_runner.run_backtest.assert_not_called() + + +def test_cloud_backtest_with_parameters() -> None: + create_fake_lean_cli_directory() + + project = create_api_project(1, "My Project") + backtest = create_api_backtest() + + api_client = mock.Mock() + api_client.projects.get_all.return_value = [project] + + cloud_runner = mock.Mock() + cloud_runner.run_backtest.return_value = backtest + initialize_container(api_client_to_use=api_client, cloud_runner_to_use=cloud_runner) + + # Run cloud backtest with --parameter option + result = CliRunner().invoke(lean, [ + "cloud", "backtest", "My Project", + "--parameter", "integer", "123", + "--parameter", "float", "456.789", + "--parameter", "string", "hello world", + "--parameter", "negative", "-42.5" + ]) + + assert result.exit_code == 0 + + cloud_runner.run_backtest.assert_called_once() + args, _ = cloud_runner.run_backtest.call_args + parameters = args[2] + + # --parameter values should be parsed correctly + assert parameters["integer"] == 123.0 + assert parameters["float"] == 456.789 + assert parameters["string"] == "hello world" + assert parameters["negative"] == -42.5 diff --git a/tests/commands/test_backtest.py b/tests/commands/test_backtest.py index e52461f1..f9de9444 100644 --- a/tests/commands/test_backtest.py +++ b/tests/commands/test_backtest.py @@ -727,3 +727,85 @@ def test_backtest_calls_lean_runner_with_paths_to_mount() -> None: False, {}, {"some-config": "/path/to/file.json"}) + + +def test_backtest_with_parameters() -> None: + create_fake_lean_cli_directory() + + # Run backtest with --parameter option + result = CliRunner().invoke(lean, [ + "backtest", "Python Project", + "--parameter", "integer", "123", + "--parameter", "float", "456.789", + "--parameter", "string", "hello world", + "--parameter", "negative", "-42.5" + ]) + + assert result.exit_code == 0 + + container.lean_runner.run_lean.assert_called_once() + args, _ = container.lean_runner.run_lean.call_args + + lean_config = args[0] + parameters = lean_config["parameters"] + + # --parameter values should be parsed correctly + assert parameters["integer"] == 123.0 + assert parameters["float"] == 456.789 + assert parameters["string"] == "hello world" + assert parameters["negative"] == -42.5 + + +def test_backtest_parameters_override_config_json() -> None: + create_fake_lean_cli_directory() + + # Add parameters in config.json + project_config_path = Path.cwd() / "Python Project" / "config.json" + current_content = project_config_path.read_text(encoding="utf-8") + config_dict = json.loads(current_content) + config_dict["parameters"] = { + "param1": 789, + "param2": 789.12 + } + project_config_path.write_text(json.dumps(config_dict, indent=4)) + + # Run backtest without --parameter -> uses config.json parameters + result = CliRunner().invoke(lean, [ + "backtest", "Python Project", + ]) + + assert result.exit_code == 0 + assert container.lean_runner.run_lean.call_count == 1 + + args, _ = container.lean_runner.run_lean.call_args + + lean_config = args[0] + parameters = lean_config["parameters"] + + # parameters from config.json should be used + assert parameters["param1"] == 789 + assert parameters["param2"] == 789.12 + + # Run backtest with --parameter -> should override config.json + result = CliRunner().invoke(lean, [ + "backtest", "Python Project", + "--parameter", "integer", "123", + "--parameter", "float", "456.789", + "--parameter", "string", "hello world", + "--parameter", "negative", "-42.5" + ]) + + assert result.exit_code == 0 + assert container.lean_runner.run_lean.call_count == 2 + + args, _ = container.lean_runner.run_lean.call_args + lean_config = args[0] + parameters = lean_config["parameters"] + + # Only CLI --parameter values should remain + assert "param1" not in parameters + assert "param2" not in parameters + assert parameters["integer"] == 123 + assert parameters["float"] == 456.789 + assert parameters["string"] == "hello world" + assert parameters["negative"] == -42.5 From ba195738a4eb856d05ffc54cf5b83128032a2fe1 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 2 Oct 2025 13:17:54 -0500 Subject: [PATCH 2/6] Solve review comments --- lean/commands/backtest.py | 10 ++++++---- lean/commands/cloud/backtest.py | 8 +++++--- lean/components/api/backtest_client.py | 3 ++- lean/components/cloud/cloud_runner.py | 6 +++--- lean/components/config/lean_config_manager.py | 16 ++++++++++++++-- lean/models/utils.py | 13 ------------- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index fdf3233f..cdc5cd37 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -285,7 +285,9 @@ def _migrate_csharp_csproj(project_dir: Path) -> None: @option("--parameter", type=(str, str), multiple=True, - help="Key-value pairs to pass as backtest parameters") + help="Key-value pairs to pass as backtest parameters. " + "Values can be string, int, or float.\n" + "Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05") def backtest(project: Path, output: Optional[Path], detach: bool, @@ -402,9 +404,9 @@ def backtest(project: Path, kwargs, logger, environment_name, container_module_version) if parameter: - from lean.models.utils import parse_parameters - parameters = parse_parameters(parameter) - logger.info(f"Using parameters from command line: {parameters}") + from lean.components.config.lean_config_manager import LeanConfigManager + parameters = LeanConfigManager.parse_parameters(parameter) + logger.debug(f"Using parameters from command line: {parameters}") lean_config["parameters"] = parameters lean_runner = container.lean_runner diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index d766ae2a..942120c2 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -30,7 +30,9 @@ @option("--parameter", type=(str, str), multiple=True, - help="Key-value pairs to pass as backtest parameters") + help="Key-value pairs to pass as backtest parameters. " + "Values can be string, int, or float.\n" + "Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05") def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, parameter: List[Tuple[str, str]]) -> None: """Backtest a project in the cloud. @@ -58,8 +60,8 @@ def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, if name is None: name = container.name_generator.generate_name() - from lean.models.utils import parse_parameters - parameters = parse_parameters(parameter) + from lean.components.config.lean_config_manager import LeanConfigManager + parameters = LeanConfigManager.parse_parameters(parameter) cloud_runner = container.cloud_runner finished_backtest = cloud_runner.run_backtest(cloud_project, name, parameters) diff --git a/lean/components/api/backtest_client.py b/lean/components/api/backtest_client.py index 53a7a9ad..76dff058 100644 --- a/lean/components/api/backtest_client.py +++ b/lean/components/api/backtest_client.py @@ -10,6 +10,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Union from lean.components.api.api_client import * from lean.models.api import QCBacktest @@ -39,7 +40,7 @@ def get(self, project_id: int, backtest_id: str) -> QCBacktest: return QCBacktest(**data["backtest"]) - def create(self, project_id: int, compile_id: str, name: str, parameters: Dict[str, any] = None) -> QCBacktest: + def create(self, project_id: int, compile_id: str, name: str, parameters: Dict[str, Union[int, float, str]] = None) -> QCBacktest: """Creates a new backtest. :param project_id: the id of the project to create a backtest for diff --git a/lean/components/cloud/cloud_runner.py b/lean/components/cloud/cloud_runner.py index a90626b1..2188fbd6 100644 --- a/lean/components/cloud/cloud_runner.py +++ b/lean/components/cloud/cloud_runner.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict +from typing import List, Dict, Union from click import confirm @@ -75,7 +75,7 @@ def is_backtest_done(self, backtest_data: QCBacktest, delay: float = 10.0): self._logger.error(f"Error checking backtest completion status for ID {backtest_data.backtestId}: {e}") raise - def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, any] = None) -> QCBacktest: + def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, Union[int, float, str]] = None) -> QCBacktest: """Runs a backtest in the cloud. :param project: the project to backtest @@ -88,7 +88,7 @@ def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, any] self._logger.info(f"Started backtest named '{name}' for project '{project.name}'") if parameters: - self._logger.info(f"Using parameters: {parameters}") + self._logger.debug(f"Using parameters: {parameters}") self._logger.info(f"Backtest url: {created_backtest.get_url()}") try: diff --git a/lean/components/config/lean_config_manager.py b/lean/components/config/lean_config_manager.py index 6c579227..0dcebfcc 100644 --- a/lean/components/config/lean_config_manager.py +++ b/lean/components/config/lean_config_manager.py @@ -13,8 +13,7 @@ from os.path import normcase, normpath from pathlib import Path -from typing import Any, Dict, Optional, List - +from typing import Any, Dict, Optional, List, Tuple, Union from lean.components.cloud.module_manager import ModuleManager from lean.components.config.cli_config_manager import CLIConfigManager @@ -353,3 +352,16 @@ def parse_json(self, content) -> Dict[str, Any]: # just in case slower fallback from json5 import loads return loads(content) + + @staticmethod + def parse_parameters(parameters: List[Tuple[str, str]]) -> Dict[str, Union[int, float, str]]: + """Parse parameters from command line to appropriate types.""" + parsed: Dict[str, Union[int, float, str]] = {} + for key, value in parameters: + try: + num = float(value) + parsed_value = int(num) if num.is_integer() else num + except ValueError: + parsed_value = value + parsed[key] = parsed_value + return parsed diff --git a/lean/models/utils.py b/lean/models/utils.py index 41166cba..7c868d01 100644 --- a/lean/models/utils.py +++ b/lean/models/utils.py @@ -13,7 +13,6 @@ from enum import Enum from pathlib import Path -from typing import List, Tuple, Dict from lean.models.pydantic import WrappedBaseModel @@ -49,15 +48,3 @@ class LeanLibraryReference(WrappedBaseModel): """The information of a library reference in a project's config.json file""" name: str path: Path - -def parse_parameters(parameters: List[Tuple[str, str]]) -> Dict[str, any]: - """Parse parameters from command line to appropriate types.""" - parsed = {} - for key, value in parameters: - try: - num = float(value) - parsed_value = int(num) if num.is_integer() else num - except ValueError: - parsed_value = value - parsed[key] = parsed_value - return parsed From 037b5a3597a72ac695610c97e8252fe6bd0f4e7e Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 2 Oct 2025 15:55:49 -0500 Subject: [PATCH 3/6] Update README.md --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dd9477ad..640be648 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,8 @@ Options: --extra-docker-config TEXT Extra docker configuration as a JSON string. For more information https://docker- py.readthedocs.io/en/stable/containers.html --no-update Use the local LEAN engine image instead of pulling the latest version + --parameter ... Key-value pairs to pass as backtest parameters. Values can be string, int, or float. + Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05 --lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json) --verbose Enable debug logging --help Show this message and exit. @@ -308,11 +310,13 @@ Usage: lean cloud backtest [OPTIONS] PROJECT use the --push option to push local modifications to the cloud before running the backtest. Options: - --name TEXT The name of the backtest (a random one is generated if not specified) - --push Push local modifications to the cloud before running the backtest - --open Automatically open the results in the browser when the backtest is finished - --verbose Enable debug logging - --help Show this message and exit. + --name TEXT The name of the backtest (a random one is generated if not specified) + --push Push local modifications to the cloud before running the backtest + --open Automatically open the results in the browser when the backtest is finished + --parameter ... Key-value pairs to pass as backtest parameters. Values can be string, int, or float. + Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05 + --verbose Enable debug logging + --help Show this message and exit. ``` _See code: [lean/commands/cloud/backtest.py](lean/commands/cloud/backtest.py)_ From 0321851eea9ec2fa6caebbed47d389559a31fdf1 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 2 Oct 2025 16:17:54 -0500 Subject: [PATCH 4/6] Add reusable --parameter decorator --- lean/click.py | 21 +++++++++++++++++++++ lean/commands/backtest.py | 9 ++------- lean/commands/cloud/backtest.py | 9 ++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/lean/click.py b/lean/click.py index b67b3936..0eb4e86d 100644 --- a/lean/click.py +++ b/lean/click.py @@ -404,3 +404,24 @@ def ensure_options(options: List[str]) -> None: You are missing the following option{"s" if len(missing_options) > 1 else ""}: {''.join(help_formatter.buffer)} """.strip()) + +def backtest_parameter_option(func: FC) -> FC: + """Decorator that adds the --parameter option to Click commands. + + This decorator can be used with both cloud and local backtest commands + to add support for passing parameters via command line. + + Example usage: + @parameter_option + def backtest(...): + ... + """ + func = option( + "--parameter", + type=(str, str), + multiple=True, + help="Key-value pairs to pass as backtest parameters. " + "Values can be string, int, or float.\n" + "Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05" + )(func) + return func \ No newline at end of file diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index cdc5cd37..ff80971b 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -15,7 +15,7 @@ from typing import List, Optional, Tuple from click import command, option, argument, Choice -from lean.click import LeanCommand, PathParameter +from lean.click import LeanCommand, PathParameter, backtest_parameter_option from lean.constants import DEFAULT_ENGINE_IMAGE, LEAN_ROOT_PATH from lean.container import container, Logger from lean.models.utils import DebuggingMethod @@ -282,12 +282,7 @@ def _migrate_csharp_csproj(project_dir: Path) -> None: is_flag=True, default=False, help="Use the local LEAN engine image instead of pulling the latest version") -@option("--parameter", - type=(str, str), - multiple=True, - help="Key-value pairs to pass as backtest parameters. " - "Values can be string, int, or float.\n" - "Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05") +@backtest_parameter_option def backtest(project: Path, output: Optional[Path], detach: bool, diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index 942120c2..4d886f94 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -13,7 +13,7 @@ from typing import List, Optional, Tuple from click import command, argument, option -from lean.click import LeanCommand +from lean.click import LeanCommand, backtest_parameter_option from lean.container import container @command(cls=LeanCommand) @@ -27,12 +27,7 @@ is_flag=True, default=False, help="Automatically open the results in the browser when the backtest is finished") -@option("--parameter", - type=(str, str), - multiple=True, - help="Key-value pairs to pass as backtest parameters. " - "Values can be string, int, or float.\n" - "Example: --parameter symbol AAPL --parameter period 10 --parameter threshold 0.05") +@backtest_parameter_option def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, parameter: List[Tuple[str, str]]) -> None: """Backtest a project in the cloud. From 294a6716aa63c15758eedb9d1af6212ee5399bec Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 3 Oct 2025 16:48:36 -0500 Subject: [PATCH 5/6] Resolve review comments --- lean/commands/backtest.py | 5 ++--- lean/commands/cloud/backtest.py | 6 ++++-- lean/components/api/backtest_client.py | 3 +-- lean/components/cloud/cloud_runner.py | 6 ++---- lean/components/config/lean_config_manager.py | 16 ++-------------- tests/commands/cloud/test_backtest.py | 6 +++--- tests/commands/test_backtest.py | 12 ++++++------ 7 files changed, 20 insertions(+), 34 deletions(-) diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index ff80971b..6fb0a17d 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -299,7 +299,7 @@ def backtest(project: Path, extra_config: Optional[Tuple[str, str]], extra_docker_config: Optional[str], no_update: bool, - parameter: List[Tuple[str, str]] = None, + parameter: List[Tuple[str, str]], **kwargs) -> None: """Backtest a project locally using Docker. @@ -399,8 +399,7 @@ def backtest(project: Path, kwargs, logger, environment_name, container_module_version) if parameter: - from lean.components.config.lean_config_manager import LeanConfigManager - parameters = LeanConfigManager.parse_parameters(parameter) + parameters = dict(parameter) logger.debug(f"Using parameters from command line: {parameters}") lean_config["parameters"] = parameters diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index 4d886f94..0b1ca74d 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -55,8 +55,10 @@ def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, if name is None: name = container.name_generator.generate_name() - from lean.components.config.lean_config_manager import LeanConfigManager - parameters = LeanConfigManager.parse_parameters(parameter) + parameters = None + if parameter: + parameters = dict(parameter) + logger.debug(f"Using parameters: {parameters}") cloud_runner = container.cloud_runner finished_backtest = cloud_runner.run_backtest(cloud_project, name, parameters) diff --git a/lean/components/api/backtest_client.py b/lean/components/api/backtest_client.py index 76dff058..5a662911 100644 --- a/lean/components/api/backtest_client.py +++ b/lean/components/api/backtest_client.py @@ -10,7 +10,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Union from lean.components.api.api_client import * from lean.models.api import QCBacktest @@ -40,7 +39,7 @@ def get(self, project_id: int, backtest_id: str) -> QCBacktest: return QCBacktest(**data["backtest"]) - def create(self, project_id: int, compile_id: str, name: str, parameters: Dict[str, Union[int, float, str]] = None) -> QCBacktest: + def create(self, project_id: int, compile_id: str, name: str, parameters: Dict[str, str] = None) -> QCBacktest: """Creates a new backtest. :param project_id: the id of the project to create a backtest for diff --git a/lean/components/cloud/cloud_runner.py b/lean/components/cloud/cloud_runner.py index 2188fbd6..d0fe47c4 100644 --- a/lean/components/cloud/cloud_runner.py +++ b/lean/components/cloud/cloud_runner.py @@ -11,7 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Dict, Union +from typing import List, Dict from click import confirm @@ -75,7 +75,7 @@ def is_backtest_done(self, backtest_data: QCBacktest, delay: float = 10.0): self._logger.error(f"Error checking backtest completion status for ID {backtest_data.backtestId}: {e}") raise - def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, Union[int, float, str]] = None) -> QCBacktest: + def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, str] = None) -> QCBacktest: """Runs a backtest in the cloud. :param project: the project to backtest @@ -87,8 +87,6 @@ def run_backtest(self, project: QCProject, name: str, parameters: Dict[str, Unio created_backtest = self._api_client.backtests.create(project.projectId, finished_compile.compileId, name, parameters) self._logger.info(f"Started backtest named '{name}' for project '{project.name}'") - if parameters: - self._logger.debug(f"Using parameters: {parameters}") self._logger.info(f"Backtest url: {created_backtest.get_url()}") try: diff --git a/lean/components/config/lean_config_manager.py b/lean/components/config/lean_config_manager.py index 0dcebfcc..6c579227 100644 --- a/lean/components/config/lean_config_manager.py +++ b/lean/components/config/lean_config_manager.py @@ -13,7 +13,8 @@ from os.path import normcase, normpath from pathlib import Path -from typing import Any, Dict, Optional, List, Tuple, Union +from typing import Any, Dict, Optional, List + from lean.components.cloud.module_manager import ModuleManager from lean.components.config.cli_config_manager import CLIConfigManager @@ -352,16 +353,3 @@ def parse_json(self, content) -> Dict[str, Any]: # just in case slower fallback from json5 import loads return loads(content) - - @staticmethod - def parse_parameters(parameters: List[Tuple[str, str]]) -> Dict[str, Union[int, float, str]]: - """Parse parameters from command line to appropriate types.""" - parsed: Dict[str, Union[int, float, str]] = {} - for key, value in parameters: - try: - num = float(value) - parsed_value = int(num) if num.is_integer() else num - except ValueError: - parsed_value = value - parsed[key] = parsed_value - return parsed diff --git a/tests/commands/cloud/test_backtest.py b/tests/commands/cloud/test_backtest.py index 53268e3a..30402c92 100644 --- a/tests/commands/cloud/test_backtest.py +++ b/tests/commands/cloud/test_backtest.py @@ -271,7 +271,7 @@ def test_cloud_backtest_with_parameters() -> None: parameters = args[2] # --parameter values should be parsed correctly - assert parameters["integer"] == 123.0 - assert parameters["float"] == 456.789 + assert parameters["integer"] == "123" + assert parameters["float"] == "456.789" assert parameters["string"] == "hello world" - assert parameters["negative"] == -42.5 + assert parameters["negative"] == "-42.5" diff --git a/tests/commands/test_backtest.py b/tests/commands/test_backtest.py index f9de9444..0d258571 100644 --- a/tests/commands/test_backtest.py +++ b/tests/commands/test_backtest.py @@ -750,10 +750,10 @@ def test_backtest_with_parameters() -> None: parameters = lean_config["parameters"] # --parameter values should be parsed correctly - assert parameters["integer"] == 123.0 - assert parameters["float"] == 456.789 + assert parameters["integer"] == "123" + assert parameters["float"] == "456.789" assert parameters["string"] == "hello world" - assert parameters["negative"] == -42.5 + assert parameters["negative"] == "-42.5" def test_backtest_parameters_override_config_json() -> None: @@ -805,7 +805,7 @@ def test_backtest_parameters_override_config_json() -> None: # Only CLI --parameter values should remain assert "param1" not in parameters assert "param2" not in parameters - assert parameters["integer"] == 123 - assert parameters["float"] == 456.789 + assert parameters["integer"] == "123" + assert parameters["float"] == "456.789" assert parameters["string"] == "hello world" - assert parameters["negative"] == -42.5 + assert parameters["negative"] == "-42.5" From 8f7d4b719b6c3397990a8ceb017ac6122236a721 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 3 Oct 2025 17:16:29 -0500 Subject: [PATCH 6/6] Centralize logs in a single location --- lean/commands/backtest.py | 5 ++--- lean/commands/cloud/backtest.py | 6 +----- lean/components/config/lean_config_manager.py | 10 ++++++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index 6fb0a17d..98d6fb3e 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -399,9 +399,8 @@ def backtest(project: Path, kwargs, logger, environment_name, container_module_version) if parameter: - parameters = dict(parameter) - logger.debug(f"Using parameters from command line: {parameters}") - lean_config["parameters"] = parameters + # Override existing parameters if any are provided via --parameter + lean_config["parameters"] = lean_config_manager.get_parameters(parameter) lean_runner = container.lean_runner lean_runner.run_lean(lean_config, diff --git a/lean/commands/cloud/backtest.py b/lean/commands/cloud/backtest.py index 0b1ca74d..15238b68 100644 --- a/lean/commands/cloud/backtest.py +++ b/lean/commands/cloud/backtest.py @@ -55,11 +55,7 @@ def backtest(project: str, name: Optional[str], push: bool, open_browser: bool, if name is None: name = container.name_generator.generate_name() - parameters = None - if parameter: - parameters = dict(parameter) - logger.debug(f"Using parameters: {parameters}") - + parameters = container.lean_config_manager.get_parameters(parameter) cloud_runner = container.cloud_runner finished_backtest = cloud_runner.run_backtest(cloud_project, name, parameters) diff --git a/lean/components/config/lean_config_manager.py b/lean/components/config/lean_config_manager.py index 6c579227..2eb3616c 100644 --- a/lean/components/config/lean_config_manager.py +++ b/lean/components/config/lean_config_manager.py @@ -13,8 +13,7 @@ from os.path import normcase, normpath from pathlib import Path -from typing import Any, Dict, Optional, List - +from typing import Any, Dict, Optional, List, Tuple from lean.components.cloud.module_manager import ModuleManager from lean.components.config.cli_config_manager import CLIConfigManager @@ -353,3 +352,10 @@ def parse_json(self, content) -> Dict[str, Any]: # just in case slower fallback from json5 import loads return loads(content) + + def get_parameters(self, parameters: List[Tuple[str, str]]) -> Dict[str, str]: + """Convert a list of (key, value) pairs into a dictionary.""" + params_dict = dict(parameters) + if parameters: + self._logger.debug(f"Using parameters: {params_dict}") + return params_dict