diff --git a/lean/commands/report.py b/lean/commands/report.py index 41b00d1c..7936f393 100644 --- a/lean/commands/report.py +++ b/lean/commands/report.py @@ -20,7 +20,7 @@ from lean.constants import DEFAULT_ENGINE_IMAGE, PROJECT_CONFIG_FILE_NAME from lean.container import container from lean.models.errors import MoreInfoError -from lean.components.util.live_utils import get_state_json +from lean.components.util.live_utils import get_latest_result_json_file def _find_project_directory(backtest_file: Path) -> Optional[Path]: @@ -108,8 +108,16 @@ def report(backtest_results: Optional[Path], if report_destination.exists() and not overwrite: raise RuntimeError(f"{report_destination} already exists, use --overwrite to overwrite it") + environment = "backtests" + output_config_manager = container.output_config_manager + output_directory = output_config_manager.get_latest_output_directory(environment) + + if output_directory is None: + raise ValueError(f"No output {environment} directories were found. " + f"Make sure you run a backtest or live deployment first.") + if backtest_results is None: - backtest_results = get_state_json("backtests") + backtest_results = get_latest_result_json_file(output_directory) if not backtest_results: raise MoreInfoError( "Could not find a recent backtest result file, please use the --backtest-results option", @@ -175,7 +183,7 @@ def report(backtest_results: Optional[Path], with config_path.open("w+", encoding="utf-8") as file: dump(report_config, file) - backtest_id = container.output_config_manager.get_backtest_id(backtest_results.parent) + backtest_id = container.output_config_manager.get_backtest_id(output_directory) lean_config_manager = container.lean_config_manager data_dir = lean_config_manager.get_data_directory() diff --git a/lean/components/config/output_config_manager.py b/lean/components/config/output_config_manager.py index d292289a..7bef99ec 100644 --- a/lean/components/config/output_config_manager.py +++ b/lean/components/config/output_config_manager.py @@ -113,6 +113,38 @@ def get_live_deployment_by_id(self, live_deployment_id: int, root_directory: Opt """ return self._get_by_id("Live deployment", live_deployment_id, ["live/*"], root_directory) + def get_latest_output_directory(self, environment: str) -> Optional[Path]: + """Finds the latest output directory for the given environment (live or backtests) + + :param environment: The environment to find the latest output directory for (live or backtests) + :return: The path to the latest output directory for the given environment + :raises RuntimeError: If no output directory is found for the given environment + """ + output_json_files = sorted(Path.cwd().rglob(f"{environment}/*/*.json"), + key=lambda d: d.stat().st_mtime, + reverse=True) + + if len(output_json_files) == 0: + return None + + return output_json_files[0].parent + + def get_output_id(self, output_directory: Path) -> Optional[int]: + """Returns the id of an output, regardless of whether it is a backtest or a live deployment. + + It will return None if the output directory does not contain any output with an existing id. + + :param output_directory: the path to the output to retrieve the id of + :return: the id of the given output + """ + output_id = self._get_id(output_directory, 9) + + if str(output_id)[0] == '9': + # no existing id was found + return None + + return output_id + def _get_id(self, output_directory: Path, prefix: int) -> int: config = self.get_output_config(output_directory) diff --git a/lean/components/util/live_utils.py b/lean/components/util/live_utils.py index bd32a4f2..0ad74248 100644 --- a/lean/components/util/live_utils.py +++ b/lean/components/util/live_utils.py @@ -14,7 +14,7 @@ from click import prompt, confirm from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from lean.components.api.api_client import APIClient from lean.components.util.logger import Logger from lean.models.brokerages.cloud import all_cloud_brokerages @@ -67,7 +67,11 @@ def _get_last_portfolio(api_client: APIClient, project_id: str, project_name: Pa last_state = api_client.get("live/read/portfolio", {"projectId": project_id}) previous_portfolio_state = last_state["portfolio"] elif cloud_last_time < local_last_time: - previous_state_file = get_state_json("live") + from lean.container import container + output_directory = container.output_config_manager.get_latest_output_directory("live") + if not output_directory: + return None + previous_state_file = get_latest_result_json_file(output_directory) if not previous_state_file: return None previous_portfolio_state = {x.lower(): y for x, y in loads(open(previous_state_file, "r", encoding="utf-8").read()).items()} @@ -198,20 +202,17 @@ def configure_initial_holdings(logger: Logger, holdings_option: LiveInitialState return _configure_initial_holdings_interactively(logger, holdings_option, previous_holdings) -def _filter_json_name_backtest(file: Path) -> bool: - return not file.name.endswith("-order-events.json") and not file.name.endswith("alpha-results.json") - +def get_latest_result_json_file(output_directory: Path) -> Optional[Path]: + from lean.container import container -def _filter_json_name_live(file: Path) -> bool: - return file.name.replace("L-", "", 1).replace(".json", "").isdigit() # The json should have name like "L-1234567890.json" + output_config_manager = container.output_config_manager + output_id = output_config_manager.get_output_id(output_directory) + if output_id is None: + return None -def get_state_json(environment: str) -> str: - json_files = list(Path.cwd().rglob(f"{environment}/*/*.json")) - name_filter = _filter_json_name_backtest if environment == "backtests" else _filter_json_name_live - filtered_json_files = [f for f in json_files if name_filter(f)] - - if len(filtered_json_files) == 0: + result_file = output_directory / f"{output_id}.json" + if not result_file.exists(): return None - return sorted(filtered_json_files, key=lambda f: f.stat().st_mtime, reverse=True)[0] + return result_file diff --git a/tests/commands/test_report.py b/tests/commands/test_report.py index 25fd2554..336f2afa 100644 --- a/tests/commands/test_report.py +++ b/tests/commands/test_report.py @@ -34,11 +34,18 @@ def setup_backtest_results() -> None: """A pytest fixture which creates a backtest results file before every test.""" create_fake_lean_cli_directory() - results_path = Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "results.json" + results_dir = Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" + + results_id = 1459804915 + results_path = results_dir / f"{results_id}.json" results_path.parent.mkdir(parents=True, exist_ok=True) with results_path.open("w+", encoding="utf-8") as file: file.write("{}") + results_config_path = results_dir / "config" + with results_config_path.open("w+", encoding="utf-8") as file: + file.write(json.dumps({'id': results_id})) + def run_image(image: DockerImage, **kwargs) -> bool: config_mount = [mount for mount in kwargs["mounts"] if mount["Target"] == "/Lean/Report/bin/Debug/config.json"][0] @@ -61,7 +68,7 @@ def test_report_runs_lean_container() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -78,7 +85,7 @@ def test_report_runs_report_creator() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -100,7 +107,7 @@ def test_report_sets_container_name() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -117,7 +124,7 @@ def test_report_runs_detached_container() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--detach"]) assert result.exit_code == 0 @@ -135,7 +142,7 @@ def test_report_mounts_report_config() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -152,7 +159,7 @@ def test_report_mounts_data_directory() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -172,7 +179,7 @@ def test_report_mounts_output_directory() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -189,7 +196,7 @@ def test_report_mounts_given_backtest_data_source_file() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -197,7 +204,7 @@ def test_report_mounts_given_backtest_data_source_file() -> None: args, kwargs = docker_manager.run_image.call_args mount = [m for m in kwargs["mounts"] if m["Target"] == "/Lean/Report/bin/Debug/backtest-data-source-file.json"][0] - assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "results.json") + assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "1459804915.json") def test_report_finds_latest_backtest_data_source_file_when_not_given() -> None: @@ -213,7 +220,7 @@ def test_report_finds_latest_backtest_data_source_file_when_not_given() -> None: args, kwargs = docker_manager.run_image.call_args mount = [m for m in kwargs["mounts"] if m["Target"] == "/Lean/Report/bin/Debug/backtest-data-source-file.json"][0] - assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "results.json") + assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "1459804915.json") def test_report_aborts_when_backtest_data_source_file_not_given_and_cannot_be_found() -> None: @@ -221,7 +228,7 @@ def test_report_aborts_when_backtest_data_source_file_not_given_and_cannot_be_fo docker_manager.run_image.side_effect = run_image initialize_container(docker_manager_to_use=docker_manager) - (Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "results.json").unlink() + (Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "1459804915.json").unlink() result = CliRunner().invoke(lean, ["report"]) @@ -237,9 +244,9 @@ def test_report_mounts_live_data_source_file_when_given() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--live-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -247,7 +254,7 @@ def test_report_mounts_live_data_source_file_when_given() -> None: args, kwargs = docker_manager.run_image.call_args mount = [m for m in kwargs["mounts"] if m["Target"] == "/Lean/Report/bin/Debug/live-data-source-file.json"][0] - assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "results.json") + assert mount["Source"] == str(Path.cwd() / "Python Project" / "backtests" / "2020-01-01_00-00-00" / "1459804915.json") def test_report_uses_project_directory_as_strategy_name_when_strategy_name_not_given() -> None: @@ -257,7 +264,7 @@ def test_report_uses_project_directory_as_strategy_name_when_strategy_name_not_g result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -277,7 +284,7 @@ def test_report_uses_given_strategy_name() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--strategy-name", "My Strategy"]) assert result.exit_code == 0 @@ -300,7 +307,7 @@ def test_report_uses_description_from_config_when_strategy_description_not_given result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -320,7 +327,7 @@ def test_report_uses_given_strategy_description() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--strategy-description", "My strategy description"]) assert result.exit_code == 0 @@ -341,7 +348,7 @@ def test_report_uses_given_strategy_version() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--strategy-version", "1.2.3"]) assert result.exit_code == 0 @@ -360,10 +367,10 @@ def test_report_uses_given_blank_name_version_description_when_not_given_and_bac docker_manager.run_image.side_effect = run_image initialize_container(docker_manager_to_use=docker_manager) - with (Path.cwd() / "results.json").open("w+", encoding="utf-8") as file: + with (Path.cwd() / "1459804915.json").open("w+", encoding="utf-8") as file: file.write("{}") - result = CliRunner().invoke(lean, ["report", "--backtest-results", "results.json"]) + result = CliRunner().invoke(lean, ["report", "--backtest-results", "1459804915.json"]) assert result.exit_code == 0 @@ -385,7 +392,7 @@ def test_report_writes_to_report_html_when_no_report_destination_given() -> None result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -399,7 +406,7 @@ def test_report_writes_to_given_report_destination() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--report-destination", "path/to/report.html"]) assert result.exit_code == 0 @@ -419,7 +426,7 @@ def test_report_aborts_when_report_destination_already_exists() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--report-destination", "path/to/report.html"]) assert result.exit_code != 0 @@ -439,7 +446,7 @@ def test_report_overwrites_report_destination_when_overwrite_flag_given() -> Non result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--report-destination", "path/to/report.html", "--overwrite"]) @@ -455,7 +462,7 @@ def test_report_aborts_when_run_image_fails() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code != 0 @@ -469,7 +476,7 @@ def test_report_forces_update_when_update_option_given() -> None: result = CliRunner().invoke(lean, ["report", - "--backtest-results", "Python Project/backtests/2020-01-01_00-00-00/results.json", + "--backtest-results", "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--update"]) assert result.exit_code == 0 @@ -487,7 +494,7 @@ def test_report_runs_custom_image_when_set_in_config() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json"]) + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json"]) assert result.exit_code == 0 @@ -506,7 +513,7 @@ def test_report_runs_custom_image_when_given_as_option() -> None: result = CliRunner().invoke(lean, ["report", "--backtest-results", - "Python Project/backtests/2020-01-01_00-00-00/results.json", + "Python Project/backtests/2020-01-01_00-00-00/1459804915.json", "--image", "custom/lean:456"]) assert result.exit_code == 0