def test_config_get_aborts_when_option_is_sensitive() -> None: container.cli_config_manager().user_id.set_value("123") result = CliRunner().invoke(lean, ["config", "get", "user-id"]) assert result.exit_code != 0 assert "123" not in result.output
def test_live_passes_custom_image_to_lean_runner_when_given_as_option() -> None: create_fake_lean_cli_directory() create_fake_environment("live-paper", True) docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) lean_runner = mock.Mock() container.lean_runner.override(providers.Object(lean_runner)) container.cli_config_manager().engine_image.set_value("custom/lean:123") result = CliRunner().invoke(lean, ["live", "Python Project", "--environment", "live-paper", "--image", "custom/lean:456"]) assert result.exit_code == 0 lean_runner.run_lean.assert_called_once_with(mock.ANY, "live-paper", Path("Python Project/main.py").resolve(), mock.ANY, DockerImage(name="custom/lean", tag="456"), None, False, False)
def test_research_adds_credentials_to_project_config() -> None: create_fake_lean_cli_directory() docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) container.cli_config_manager().user_id.set_value("123") container.cli_config_manager().api_token.set_value("456") result = CliRunner().invoke(lean, ["research", "Python Project"]) assert result.exit_code == 0 docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args mount = [ m for m in kwargs["mounts"] if m["Target"] == "/Lean/Launcher/bin/Debug/Notebooks/config.json" ][0] with open(mount["Source"]) as file: config = json.load(file) assert config["job-user-id"] == "123" assert config["api-access-token"] == "456"
def test_config_unset_removes_the_value_of_the_option() -> None: container.cli_config_manager().user_id.set_value("12345") result = CliRunner().invoke(lean, ["config", "unset", "user-id"]) assert result.exit_code == 0 assert container.cli_config_manager().user_id.get_value() is None
def test_config_get_prints_the_value_of_the_option_with_the_given_key() -> None: container.cli_config_manager().default_language.set_value("python") runner = CliRunner() result = runner.invoke(lean, ["config", "get", "default-language"]) assert result.exit_code == 0 assert result.output == "python\n"
def test_logout_deletes_credentials_storage_file() -> None: container.cli_config_manager().user_id.set_value("123") assert Path("~/.lean/credentials").expanduser().exists() result = CliRunner().invoke(lean, ["logout"]) assert result.exit_code == 0 assert not Path("~/.lean/credentials").expanduser().exists()
def test_create_project_creates_python_project_when_default_language_set_to_python( ) -> None: container.cli_config_manager().default_language.set_value("python") result = CliRunner().invoke(lean, ["create-project", "My First Project"]) assert result.exit_code == 0 assert_python_project_exists()
def test_login_aborts_when_credentials_are_invalid() -> None: api_client = mock.Mock() api_client.is_authenticated.return_value = False container.api_client.override(providers.Object(api_client)) result = CliRunner().invoke( lean, ["login", "--user-id", "123", "--api-token", "456"]) assert result.exit_code != 0 assert container.cli_config_manager().user_id.get_value() is None assert container.cli_config_manager().api_token.get_value() is None
def test_login_logs_in_with_options_when_given() -> None: api_client = mock.Mock() api_client.is_authenticated.return_value = True container.api_client.override(providers.Object(api_client)) result = CliRunner().invoke( lean, ["login", "--user-id", "123", "--api-token", "456"]) assert result.exit_code == 0 assert container.cli_config_manager().user_id.get_value() == "123" assert container.cli_config_manager().api_token.get_value() == "456"
def test_login_prompts_when_api_token_not_given() -> None: api_client = mock.Mock() api_client.is_authenticated.return_value = True container.api_client.override(providers.Object(api_client)) result = CliRunner().invoke(lean, ["login", "--user-id", "123"], input="456\n") assert result.exit_code == 0 assert "API token:" in result.output assert container.cli_config_manager().user_id.get_value() == "123" assert container.cli_config_manager().api_token.get_value() == "456"
def test_config_list_does_not_show_complete_values_of_sensitive_options( size) -> None: container.cli_config_manager().user_id.set_value("123") container.cli_config_manager().api_token.set_value( "abcdefghijklmnopqrstuvwxyz") size.return_value = ConsoleDimensions(1000, 1000) result = CliRunner().invoke(lean, ["config", "list"]) assert result.exit_code == 0 assert "123" not in result.output assert "abcdefghijklmnopqrstuvwxyz" not in result.output
def login(user_id: Optional[str], api_token: Optional[str]) -> None: """Log in with a QuantConnect account. If user id or API token is not provided an interactive prompt will show. Credentials are stored in ~/.lean/credentials and are removed upon running `lean logout`. """ logger = container.logger() credentials_storage = container.credentials_storage() if user_id is None or api_token is None: logger.info( "Your user id and API token are needed to make authenticated requests to the QuantConnect API" ) logger.info( "You can request these credentials on https://www.quantconnect.com/account" ) logger.info(f"Both will be saved in {credentials_storage.file}") if user_id is None: user_id = click.prompt("User id") if api_token is None: api_token = click.prompt("API token") api_client = container.api_client(user_id=user_id, api_token=api_token) if not api_client.is_authenticated(): raise RuntimeError("Credentials are invalid") cli_config_manager = container.cli_config_manager() cli_config_manager.user_id.set_value(user_id) cli_config_manager.api_token.set_value(api_token) logger.info("Successfully logged in")
def whoami() -> None: """Display who is logged in.""" logger = container.logger() api_client = container.api_client() cli_config_manager = container.cli_config_manager() if cli_config_manager.user_id.get_value( ) is not None and cli_config_manager.api_token.get_value() is not None: try: organizations = api_client.organizations.get_all() logged_in = True except AuthenticationError: logged_in = False else: logged_in = False if not logged_in: logger.info("You are not logged in") return personal_organization_id = next(o.id for o in organizations if o.ownerName == "You") personal_organization = api_client.organizations.get( personal_organization_id) member = next(m for m in personal_organization.members if m.isAdmin) logger.info(f"You are logged in as {member.name} ({member.email})")
def test_config_set_updates_the_value_of_the_option() -> None: runner = CliRunner() result = runner.invoke(lean, ["config", "set", "user-id", "12345"]) assert result.exit_code == 0 assert container.cli_config_manager().user_id.get_value() == "12345"
def test_init_prompts_for_default_language_when_not_set_yet() -> None: result = CliRunner().invoke(lean, ["init"], input="csharp\n") assert result.exit_code == 0 assert container.cli_config_manager().default_language.get_value( ) == "csharp"
def test_research_runs_custom_image_when_given_as_option() -> None: create_fake_lean_cli_directory() docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) container.cli_config_manager().research_image.set_value("custom/research:123") result = CliRunner().invoke(lean, ["research", "Python Project", "--image", "custom/research:456"]) assert result.exit_code == 0 docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args assert args[0] == DockerImage(name="custom/research", tag="456")
def create_project(name: str, language: str) -> None: """Create a new project containing starter code. If NAME is a path containing subdirectories those will be created automatically. The default language can be set using `lean config set default-language python/csharp`. """ cli_config_manager = container.cli_config_manager() language = language if language is not None else cli_config_manager.default_language.get_value( ) if language is None: raise MoreInfoError( "Please specify a language with --language or set the default language using `lean config set default-language python/csharp`", "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/project-management" ) full_path = Path.cwd() / name path_validator = container.path_validator() if not path_validator.is_path_valid(full_path): raise MoreInfoError( f"'{name}' is not a valid path", "https://www.quantconnect.com/docs/v2/lean-cli/user-guides/troubleshooting#02-Common-errors" ) if full_path.exists(): raise RuntimeError( f"A project named '{name}' already exists, please choose a different name" ) else: project_manager = container.project_manager() project_manager.create_new_project( full_path, QCLanguage.Python if language == "python" else QCLanguage.CSharp) # Convert the project name into a valid class name by removing all non-alphanumeric characters class_name = re.sub(f"[^a-zA-Z0-9]", "", "".join(map(_capitalize, name.split(" ")))) if language == "python": with (full_path / "main.py").open("w+", encoding="utf-8") as file: file.write(DEFAULT_PYTHON_MAIN.replace("$NAME$", class_name)) else: with (full_path / "Main.cs").open("w+", encoding="utf-8") as file: file.write(DEFAULT_CSHARP_MAIN.replace("$NAME$", class_name)) with (full_path / "research.ipynb").open("w+", encoding="utf-8") as file: file.write(DEFAULT_PYTHON_NOTEBOOK if language == "python" else DEFAULT_CSHARP_NOTEBOOK) with (full_path / "config.json").open("w+", encoding="utf-8") as file: file.write(DEFAULT_PYTHON_CONFIG if language == "python" else DEFAULT_CSHARP_CONFIG) logger = container.logger() logger.info( f"Successfully created {'Python' if language == 'python' else 'C#'} project '{name}'" )
def live(project: Path, environment: str, output: Optional[Path], image: Optional[str], update: bool) -> None: """Start live trading a project locally using Docker. \b If PROJECT is a directory, the algorithm in the main.py or Main.cs file inside it will be executed. If PROJECT is a file, the algorithm in the specified file will be executed. \b ENVIRONMENT must be the name of an environment in the Lean configuration file with live-mode set to true. By default the official LEAN engine image is used. You can override this using the --image option. Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`. """ project_manager = container.project_manager() algorithm_file = project_manager.find_algorithm_file(Path(project)) if output is None: output = algorithm_file.parent / "live" / datetime.now().strftime( "%Y-%m-%d_%H-%M-%S") lean_config_manager = container.lean_config_manager() lean_config = lean_config_manager.get_complete_lean_config( environment, algorithm_file, None) if "environments" not in lean_config or environment not in lean_config[ "environments"]: lean_config_path = lean_config_manager.get_lean_config_path() raise MoreInfoError( f"{lean_config_path} does not contain an environment named '{environment}'", "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/live-trading/local-live-trading" ) if not lean_config["environments"][environment]["live-mode"]: raise MoreInfoError( f"The '{environment}' is not a live trading environment (live-mode is set to false)", "https://www.quantconnect.com/docs/v2/lean-cli/tutorials/live-trading/local-live-trading" ) _raise_for_missing_properties(lean_config, environment, lean_config_manager.get_lean_config_path()) _start_iqconnect_if_necessary(lean_config, environment) cli_config_manager = container.cli_config_manager() engine_image = cli_config_manager.get_engine_image(image) docker_manager = container.docker_manager() if update or not docker_manager.supports_dotnet_5(engine_image): docker_manager.pull_image(engine_image) lean_runner = container.lean_runner() lean_runner.run_lean(environment, algorithm_file, output, engine_image, None) if str(engine_image) == DEFAULT_ENGINE_IMAGE and not update: update_manager = container.update_manager() update_manager.warn_if_docker_image_outdated(engine_image)
def build(root: Path, tag: str) -> None: """Build Docker images of your own version of LEAN and the Alpha Streams SDK. \b ROOT must point to a directory containing the LEAN repository and the Alpha Streams SDK repository: https://github.com/QuantConnect/Lean & https://github.com/QuantConnect/AlphaStreams When ROOT is not given, the current directory is used as root directory. \b This command performs the following actions: 1. The lean-cli/foundation:latest image is built from Lean/DockerfileLeanFoundation(ARM). 2. LEAN is compiled in a Docker container using the lean-cli/foundation:latest image. 3. The Alpha Streams SDK is compiled in a Docker container using the lean-cli/foundation:latest image. 4. The lean-cli/engine:latest image is built from Lean/Dockerfile using lean-cli/foundation:latest as base image. 5. The lean-cli/research:latest image is built from Lean/DockerfileJupyter using lean-cli/engine:latest as base image. 6. The default engine image is set to lean-cli/engine:latest. 7. The default research image is set to lean-cli/research:latest. """ lean_dir = root / "Lean" if not lean_dir.is_dir(): raise RuntimeError( f"Please clone https://github.com/QuantConnect/Lean to '{lean_dir}'" ) alpha_streams_dir = root / "AlphaStreams" if not lean_dir.is_dir(): raise RuntimeError( f"Please clone https://github.com/QuantConnect/AlphaStreams to '{alpha_streams_dir}'" ) (root / "DataLibraries").mkdir(exist_ok=True) if platform.machine() in ["arm64", "aarch64"]: foundation_dockerfile = lean_dir / "DockerfileLeanFoundationARM" else: foundation_dockerfile = lean_dir / "DockerfileLeanFoundation" custom_foundation_image = DockerImage(name="lean-cli/foundation", tag=tag) custom_engine_image = DockerImage(name="lean-cli/engine", tag=tag) custom_research_image = DockerImage(name="lean-cli/research", tag=tag) _build_image(root, foundation_dockerfile, None, custom_foundation_image) _compile_csharp(root, lean_dir, custom_foundation_image) _compile_csharp(root, alpha_streams_dir, custom_foundation_image) _build_image(root, lean_dir / "Dockerfile", custom_foundation_image, custom_engine_image) _build_image(root, lean_dir / "DockerfileJupyter", custom_engine_image, custom_research_image) logger = container.logger() cli_config_manager = container.cli_config_manager() logger.info(f"Setting default engine image to '{custom_engine_image}'") cli_config_manager.engine_image.set_value(str(custom_engine_image)) logger.info(f"Setting default research image to '{custom_research_image}'") cli_config_manager.research_image.set_value(str(custom_research_image))
def test_report_runs_custom_image_when_set_in_config() -> None: docker_manager = mock.Mock() docker_manager.run_image.side_effect = run_image container.docker_manager.override(providers.Object(docker_manager)) container.cli_config_manager().engine_image.set_value("custom/lean:123") result = CliRunner().invoke(lean, [ "report", "--backtest-results", "Python Project/backtests/2020-01-01_00-00-00/results.json" ]) assert result.exit_code == 0 docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args assert args[0] == DockerImage(name="custom/lean", tag="123")
def create_project(name: str, language: str) -> None: """Create a new project containing starter code. If NAME is a path containing subdirectories those will be created automatically. The default language can be set using `lean config set default-language python/csharp`. """ cli_config_manager = container.cli_config_manager() language = language if language is not None else cli_config_manager.default_language.get_value() if language is None: raise MoreInfoError( "Please specify a language with --language or set the default language using `lean config set default-language python/csharp`", "https://www.lean.io/docs/lean-cli/projects/project-management") full_path = Path.cwd() / name if not container.path_manager().is_path_valid(full_path): raise MoreInfoError(f"'{name}' is not a valid path", "https://www.lean.io/docs/lean-cli/key-concepts/troubleshooting#02-Common-Errors") is_library_project = False try: library_dir = container.lean_config_manager().get_cli_root_directory() / "Library" is_library_project = library_dir in full_path.parents except: # get_cli_root_directory() raises an error if there is no such directory pass if is_library_project and language == "python" and not full_path.name.isidentifier(): raise RuntimeError( f"'{full_path.name}' is not a valid Python identifier, which is required for Python library projects to be importable") if full_path.exists(): raise RuntimeError(f"A project named '{name}' already exists, please choose a different name") else: project_manager = container.project_manager() project_manager.create_new_project(full_path, QCLanguage.Python if language == "python" else QCLanguage.CSharp) # Convert the project name into a valid class name by removing all non-alphanumeric characters class_name = re.sub(f"[^a-zA-Z0-9]", "", "".join(map(_capitalize, full_path.name.split(" ")))) if language == "python": main_name = "main.py" main_content = DEFAULT_PYTHON_MAIN if not is_library_project else LIBRARY_PYTHON_MAIN else: main_name = "Main.cs" main_content = DEFAULT_CSHARP_MAIN if not is_library_project else LIBRARY_CSHARP_MAIN with (full_path / main_name).open("w+", encoding="utf-8") as file: file.write(main_content.replace("$CLASS_NAME$", class_name).replace("$PROJECT_NAME$", full_path.name)) with (full_path / "research.ipynb").open("w+", encoding="utf-8") as file: file.write(DEFAULT_PYTHON_NOTEBOOK if language == "python" else DEFAULT_CSHARP_NOTEBOOK) logger = container.logger() logger.info(f"Successfully created {'Python' if language == 'python' else 'C#'} project '{name}'")
def test_data_generate_runs_custom_image_when_set_in_config() -> None: create_fake_lean_cli_directory() docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) container.cli_config_manager().engine_image.set_value("custom/lean:123") result = CliRunner().invoke( lean, ["data", "generate", "--start", "20200101", "--symbol-count", "1"]) assert result.exit_code == 0 docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args assert args[0] == DockerImage(name="custom/lean", tag="123")
def test_config_list_lists_all_options(size) -> None: size.return_value = ConsoleDimensions(1000, 1000) result = CliRunner().invoke(lean, ["config", "list"]) assert result.exit_code == 0 for option in container.cli_config_manager().all_options: assert option.key in result.output assert option.description in result.output
def test_optimize_runs_custom_image_when_set_in_config() -> None: create_fake_lean_cli_directory() docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) container.cli_config_manager().engine_image.set_value("custom/lean:123") Storage(str(Path.cwd() / "Python Project" / "config.json")).set( "parameters", {"param1": "1"}) result = CliRunner().invoke(lean, ["optimize", "Python Project"]) assert result.exit_code == 0 docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args assert args[0] == DockerImage(name="custom/lean", tag="123")
def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config( ) -> None: create_fake_lean_cli_directory() docker_manager = mock.Mock() container.docker_manager.override(providers.Object(docker_manager)) lean_runner = mock.Mock() container.lean_runner.override(providers.Object(lean_runner)) container.cli_config_manager().engine_image.set_value("custom/lean:123") result = CliRunner().invoke(lean, ["backtest", "Python Project"]) assert result.exit_code == 0 lean_runner.run_lean.assert_called_once_with( "backtesting", Path("Python Project/main.py").resolve(), mock.ANY, DockerImage(name="custom/lean", tag="123"), None)
def set(key: str, value: str) -> None: """Set a configurable option. Run `lean config list` to show all available options. """ cli_config_manager = container.cli_config_manager() option = cli_config_manager.get_option_by_key(key) option.set_value(value) click.echo(f"Successfully updated the value of '{key}' to '{option.get_value()}'")
def unset(key: str) -> None: """Unset a configurable option. Run `lean config list` to show all available options. """ cli_config_manager = container.cli_config_manager() option = cli_config_manager.get_option_by_key(key) option.unset() logger = container.logger() logger.info(f"Successfully unset '{key}'")
def backtest(project: Path, output: Optional[Path], debug: Optional[str], image: Optional[str], update: bool) -> None: """Backtest a project locally using Docker. \b If PROJECT is a directory, the algorithm in the main.py or Main.cs file inside it will be executed. If PROJECT is a file, the algorithm in the specified file will be executed. \b Go to the following url to learn how to debug backtests locally using the Lean CLI: https://www.quantconnect.com/docs/v2/lean-cli/tutorials/backtesting/debugging-local-backtests By default the official LEAN engine image is used. You can override this using the --image option. Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`. """ project_manager = container.project_manager() algorithm_file = project_manager.find_algorithm_file(Path(project)) if output is None: output = algorithm_file.parent / "backtests" / datetime.now().strftime( "%Y-%m-%d_%H-%M-%S") debugging_method = None if debug == "pycharm": debugging_method = DebuggingMethod.PyCharm _migrate_dotnet_5_python_pycharm(algorithm_file.parent) elif debug == "ptvsd": debugging_method = DebuggingMethod.PTVSD _migrate_dotnet_5_python_vscode(algorithm_file.parent) elif debug == "vsdbg": debugging_method = DebuggingMethod.VSDBG _migrate_dotnet_5_csharp_vscode(algorithm_file.parent) elif debug == "rider": debugging_method = DebuggingMethod.Rider _migrate_dotnet_5_csharp_rider(algorithm_file.parent) cli_config_manager = container.cli_config_manager() engine_image = cli_config_manager.get_engine_image(image) docker_manager = container.docker_manager() if update or not docker_manager.supports_dotnet_5(engine_image): docker_manager.pull_image(engine_image) lean_runner = container.lean_runner() lean_runner.run_lean("backtesting", algorithm_file, output, engine_image, debugging_method) if str(engine_image) == DEFAULT_ENGINE_IMAGE and not update: update_manager = container.update_manager() update_manager.warn_if_docker_image_outdated(engine_image)
def list() -> None: """List the configurable options and their current values.""" table = Table(box=box.SQUARE) table.add_column("Key") table.add_column("Value") table.add_column("Location") table.add_column("Description") for option in container.cli_config_manager().all_options: value = option.get_value(default="<not set>") # Mask values of sensitive options if value != "<not set>" and option.is_sensitive: value = "*" * 12 + value[-3:] if len(value) >= 5 else "*" * 15 table.add_row(option.key, value, str(option.location), option.description) logger = container.logger() logger.info(table)
def get(key: str) -> None: """Get the current value of a configurable option. Sensitive options like credentials cannot be retrieved this way for security reasons. Please open ~/.lean/credentials if you want to see your currently stored credentials. Run `lean config list` to show all available options. """ cli_config_manager = container.cli_config_manager() option = cli_config_manager.get_option_by_key(key) if option.is_sensitive: raise RuntimeError( "Sensitive options like credentials cannot be retrieved using `lean config get` for security reasons" ) value = option.get_value() if value is None: raise RuntimeError( f"The option with key '{key}' doesn't have a value set") click.echo(value)