From e70ddc627b96f868bbcd92976707dbe7669a745e Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 15:49:20 -0400 Subject: [PATCH 1/8] feature: add `lean ui` terminal UI for browsing projects and running commands Adds a keyboard-driven terminal UI built on Textual. It lists local projects under the Lean CLI root and, when logged in, the organization's cloud projects, labelled local, cloud or synced. Projects can be filtered and navigated with the arrow keys or j/k. The selected project offers backtest, cloud backtest, logs, push, pull and cloud status actions. Each one shows the exact `lean` command for confirmation, runs it as a child process (`python -m lean`) and streams its output into the UI. `x` stops a running command via Ctrl+C (Ctrl+Break on Windows), and pressing it again kills the process. Textual is only imported by `lean ui`, so other commands start as fast as before. textual-plotext is added for the upcoming backtest charts. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 20 +++ lean/__main__.py | 25 +++ lean/commands/__init__.py | 2 + lean/commands/ui.py | 36 +++++ lean/main.py | 9 +- lean/ui/__init__.py | 12 ++ lean/ui/actions.py | 124 ++++++++++++++ lean/ui/app.py | 328 ++++++++++++++++++++++++++++++++++++++ lean/ui/projects.py | 147 +++++++++++++++++ setup.py | 2 + tests/commands/test_ui.py | 228 ++++++++++++++++++++++++++ 11 files changed, 930 insertions(+), 3 deletions(-) create mode 100644 lean/__main__.py create mode 100644 lean/commands/ui.py create mode 100644 lean/ui/__init__.py create mode 100644 lean/ui/actions.py create mode 100644 lean/ui/app.py create mode 100644 lean/ui/projects.py create mode 100644 tests/commands/test_ui.py diff --git a/README.md b/README.md index 6b676bcbc..577e04b1d 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ The following CLI configurations are available. Use the [`lean config list`](#le - [`lean project-delete`](#lean-project-delete) - [`lean report`](#lean-report) - [`lean research`](#lean-research) +- [`lean ui`](#lean-ui) - [`lean whoami`](#lean-whoami) ### `lean backtest` @@ -2290,6 +2291,25 @@ Options: _See code: [lean/commands/research.py](lean/commands/research.py)_ +### `lean ui` + +Browse projects and run commands on them in an interactive terminal UI. + +``` +Usage: lean ui [OPTIONS] + + Browse projects and run commands on them in an interactive terminal UI. + + Use the arrow keys or j/k to move, / to filter, enter to pick an action and q to quit. + +Options: + --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. +``` + +_See code: [lean/commands/ui.py](lean/commands/ui.py)_ + ### `lean whoami` Display who is logged in. diff --git a/lean/__main__.py b/lean/__main__.py new file mode 100644 index 000000000..e206fb374 --- /dev/null +++ b/lean/__main__.py @@ -0,0 +1,25 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +# Allows running the CLI with `python -m lean`, which is how `lean ui` launches commands as child processes + +import signal + +# `lean ui` starts commands in their own process group on Windows and stops them with Ctrl+Break, +# which is turned into Ctrl+C here so the command's own Ctrl+C handling (e.g. stopping its Docker container) runs +if hasattr(signal, "SIGBREAK"): + signal.signal(signal.SIGBREAK, lambda *_: signal.raise_signal(signal.SIGINT)) + +from lean.main import main + +main(prog_name="lean") diff --git a/lean/commands/__init__.py b/lean/commands/__init__.py index dfa40a41e..5ce29158e 100644 --- a/lean/commands/__init__.py +++ b/lean/commands/__init__.py @@ -32,6 +32,7 @@ from lean.commands.research import research from lean.commands.whoami import whoami from lean.commands.gui import gui +from lean.commands.ui import ui from lean.commands.object_store import object_store from lean.commands.private_cloud import private_cloud @@ -55,5 +56,6 @@ lean.add_command(build) lean.add_command(logs) lean.add_command(gui) +lean.add_command(ui) lean.add_command(object_store) lean.add_command(private_cloud) diff --git a/lean/commands/ui.py b/lean/commands/ui.py new file mode 100644 index 000000000..8b51cb081 --- /dev/null +++ b/lean/commands/ui.py @@ -0,0 +1,36 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 click import command + +from lean.click import LeanCommand +from lean.container import container + + +@command(cls=LeanCommand, requires_lean_config=True) +def ui() -> None: + """Browse projects and run commands on them in an interactive terminal UI. + + \b + Use the arrow keys or j/k to move, / to filter, enter to pick an action and q to quit. + """ + # Textual is imported here so it does not slow down the startup of other commands + from lean.ui.app import LeanApp + + lean_config_manager = container.lean_config_manager + try: + data_dir = lean_config_manager.get_data_directory() + except Exception: + data_dir = None + + LeanApp(lean_config_manager.get_cli_root_directory(), data_dir).run() diff --git a/lean/main.py b/lean/main.py index 061c721b2..dce9ba37e 100644 --- a/lean/main.py +++ b/lean/main.py @@ -88,10 +88,13 @@ def _ensure_win32_available() -> None: from lean.container import container -def main() -> None: - """This function is the entrypoint when running a Lean command in a terminal.""" +def main(prog_name: str = None) -> None: + """This function is the entrypoint when running a Lean command in a terminal. + + :param prog_name: the program name shown in help and error messages, detected from the command line if not set + """ try: - lean.main(standalone_mode=False) + lean.main(standalone_mode=False, prog_name=prog_name) temp_manager = container.temp_manager if temp_manager.delete_temporary_directories_when_done: diff --git a/lean/ui/__init__.py b/lean/ui/__init__.py new file mode 100644 index 000000000..9af974c07 --- /dev/null +++ b/lean/ui/__init__.py @@ -0,0 +1,12 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. diff --git a/lean/ui/actions.py b/lean/ui/actions.py new file mode 100644 index 000000000..fb4301bb0 --- /dev/null +++ b/lean/ui/actions.py @@ -0,0 +1,124 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from shlex import quote +from typing import Callable, List, Optional + +from lean.ui.projects import UIProject + + +@dataclass(frozen=True) +class ProjectAction: + """A Lean CLI command that can be run on a project from the UI.""" + + key: str + label: str + needs_local: bool + needs_cloud_project: bool + needs_login: bool + refreshes_projects: bool + build_args: Callable[[UIProject], List[str]] + + def unavailable_reason(self, project: UIProject, logged_in: bool) -> Optional[str]: + """Returns why the action cannot run on the project, or None if it can.""" + if self.needs_local and not project.is_local: + return "pull it first" + if self.needs_login and not logged_in: + return "run `lean login` first" + if self.needs_cloud_project and not project.is_cloud: + return "push it first" + return None + + +def _cloud_name(project: UIProject) -> str: + """Cloud commands accept the project id, which avoids local and cloud names differing.""" + return str(project.cloud_id) if project.is_cloud else project.name + + +ACTIONS = [ + ProjectAction("b", "Backtest locally", True, False, False, False, + lambda p: ["backtest", p.name]), + ProjectAction("c", "Backtest in the cloud", False, False, True, True, + lambda p: ["cloud", "backtest", p.name, "--push"] if p.is_local + else ["cloud", "backtest", _cloud_name(p)]), + ProjectAction("l", "Show latest backtest logs", True, False, False, False, + lambda p: ["logs", "--backtest", "--project", p.name]), + ProjectAction("p", "Push to the cloud", True, False, True, True, + lambda p: ["cloud", "push", "--project", p.name]), + ProjectAction("u", "Pull from the cloud", False, True, True, True, + lambda p: ["cloud", "pull", "--project", _cloud_name(p)]), + ProjectAction("s", "Cloud live status", False, True, True, False, + lambda p: ["cloud", "status", _cloud_name(p)]), +] + + +def format_command(args: List[str]) -> str: + """Formats arguments as the `lean` command a user would type.""" + return " ".join(["lean"] + [quote(a) for a in args]) + + +class CommandProcess: + """Runs a Lean CLI command as a child process, so its console output cannot draw over the UI.""" + + def __init__(self, args: List[str], cwd: Path) -> None: + self.args = args + self._cwd = cwd + self._process: Optional[subprocess.Popen] = None + self._stop_requested = False + + def run(self, on_line: Callable[[str], None]) -> int: + """Runs the command to completion, passing each line of output to on_line. Blocks the calling thread. + + :return: the exit code of the command + """ + env = dict(os.environ, PYTHONIOENCODING="utf-8", PYTHONUNBUFFERED="1", COLUMNS="120") + creation_flags = subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 + + self._process = subprocess.Popen([sys.executable, "-m", "lean"] + self.args, + cwd=self._cwd, + env=env, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + errors="replace", + creationflags=creation_flags) + + for line in self._process.stdout: + on_line(line.rstrip("\r\n")) + + return self._process.wait() + + def stop(self) -> None: + """Asks the command to stop the way Ctrl+C would, so it can clean up its Docker containers. + + Calling this again while the command is still running kills it. + """ + if self._process is None or self._process.poll() is not None: + return + + if self._stop_requested: + self._process.kill() + return + self._stop_requested = True + + import signal + if sys.platform == "win32": + self._process.send_signal(signal.CTRL_BREAK_EVENT) + else: + self._process.send_signal(signal.SIGINT) diff --git a/lean/ui/app.py b/lean/ui/app.py new file mode 100644 index 000000000..c5c966923 --- /dev/null +++ b/lean/ui/app.py @@ -0,0 +1,328 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 pathlib import Path +from typing import List, Optional + +from rich.table import Table +from rich.text import Text +from textual import on, work +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical +from textual.screen import ModalScreen +from textual.widgets import DataTable, Footer, Header, Input, OptionList, RichLog, Static +from textual.widgets.option_list import Option + +from lean.ui.actions import ACTIONS, CommandProcess, ProjectAction, format_command +from lean.ui.projects import (UIProject, discover_local_projects, fetch_cloud_projects, filter_projects, + is_logged_in, merge_cloud_projects) + + +class ProjectTable(DataTable): + BINDINGS = [ + Binding("j", "cursor_down", show=False), + Binding("k", "cursor_up", show=False), + ] + + +class ActionList(OptionList): + BINDINGS = [ + Binding("j", "cursor_down", show=False), + Binding("k", "cursor_up", show=False), + ] + + +class ConfirmScreen(ModalScreen[bool]): + """Shows the command an action is about to run and asks for confirmation.""" + + BINDINGS = [ + Binding("y,enter", "confirm", "Run"), + Binding("n,escape", "cancel", "Cancel"), + ] + + def __init__(self, command: str) -> None: + super().__init__() + self._command = command + + def compose(self) -> ComposeResult: + message = Text.assemble(("Run this command?\n\n", "bold"), (self._command, "cyan"), + ("\n\n", ""), ("y", "bold"), (" / enter to run, ", "dim"), + ("n", "bold"), (" / esc to cancel", "dim")) + yield Static(message, id="confirm") + + def action_confirm(self) -> None: + self.dismiss(True) + + def action_cancel(self) -> None: + self.dismiss(False) + + +class LeanApp(App): + """A keyboard-driven terminal UI for browsing projects and running Lean CLI commands on them.""" + + TITLE = "LEAN" + + CSS = """ + #sidebar { width: 2fr; min-width: 32; } + #main { width: 3fr; } + #filter { border: round $primary-darken-2; } + #filter:focus { border: round $accent; } + ProjectTable, #details, ActionList, #output { border: round $primary-darken-2; } + ProjectTable:focus, ActionList:focus, #output:focus { border: round $accent; } + ProjectTable { height: 1fr; overflow-x: hidden; } + #details { height: auto; padding: 0 1; } + ActionList { height: auto; max-height: 10; } + #output { height: 1fr; } + ConfirmScreen { align: center middle; } + #confirm { width: auto; max-width: 90%; padding: 1 2; border: round $accent; background: $surface; } + """ + + BINDINGS = [ + Binding("q", "quit", "Quit"), + Binding("slash", "focus_filter", "Filter"), + Binding("r", "refresh", "Refresh"), + Binding("x", "stop_command", "Stop command"), + Binding("escape", "back", "Back", show=False), + ] + [Binding(a.key, f"run_action('{a.key}')", a.label, show=False) for a in ACTIONS] + + def __init__(self, root: Path, data_dir: Optional[Path] = None) -> None: + super().__init__() + self._root = root + self._data_dir = data_dir + self._projects: List[UIProject] = [] + self._visible: List[UIProject] = [] + self._logged_in = False + self._process: Optional[CommandProcess] = None + + def compose(self) -> ComposeResult: + yield Header() + with Horizontal(): + with Vertical(id="sidebar"): + yield Input(placeholder="type to filter projects", id="filter") + yield ProjectTable(cursor_type="row", zebra_stripes=True) + with Vertical(id="main"): + yield Static(id="details") + yield ActionList() + yield RichLog(id="output", wrap=True, markup=False, min_width=20) + yield Footer() + + def on_mount(self) -> None: + self.sub_title = str(self._root) + + table = self.query_one(ProjectTable) + table.add_columns("Where", "Lang", "Project") + table.border_title = "Projects" + self.query_one("#filter", Input).border_title = "Filter (/)" + self.query_one("#details", Static).border_title = "Project" + self.query_one(ActionList).border_title = "Actions" + self.query_one("#output", RichLog).border_title = "Output" + + table.focus() + self.action_refresh() + + # Loading projects + + def action_refresh(self) -> None: + self._load_projects() + + @work(thread=True, exclusive=True, group="projects") + def _load_projects(self) -> None: + skip_dirs = [self._data_dir] if self._data_dir is not None else [] + local_projects = discover_local_projects(self._root, skip_dirs) + logged_in = is_logged_in() + self.call_from_thread(self._set_projects, local_projects, logged_in, + "loading cloud projects..." if logged_in else "not logged in, showing local projects") + + if not logged_in: + return + + try: + cloud_projects = fetch_cloud_projects() + except Exception as exception: + self.call_from_thread(self._set_projects, local_projects, logged_in, "cloud projects unavailable") + self.call_from_thread(self.notify, f"Could not load cloud projects: {exception}", severity="error") + return + + projects = merge_cloud_projects(local_projects, cloud_projects) + self.call_from_thread(self._set_projects, projects, logged_in, f"{len(cloud_projects)} cloud projects") + + def _set_projects(self, projects: List[UIProject], logged_in: bool, status: str) -> None: + self._projects = projects + self._logged_in = logged_in + self.query_one(ProjectTable).border_subtitle = status + self._apply_filter() + + # Filtering and selection + + @on(Input.Changed, "#filter") + def _filter_changed(self) -> None: + self._apply_filter() + + @on(Input.Submitted, "#filter") + def _filter_submitted(self) -> None: + self.query_one(ProjectTable).focus() + + def _apply_filter(self) -> None: + table = self.query_one(ProjectTable) + selected = self.selected_project + + self._visible = filter_projects(self._projects, self.query_one("#filter", Input).value) + + table.clear() + for index, project in enumerate(self._visible): + location_style = {"synced": "green", "local": "yellow", "cloud": "cyan"}[project.location] + name = Text(project.name, style="dim" if project.is_library else "") + table.add_row(Text(project.location, style=location_style), project.language, name, key=str(index)) + + table.border_title = f"Projects ({len(self._visible)}/{len(self._projects)})" + + if selected is not None: + for index, project in enumerate(self._visible): + if project.name == selected.name and project.cloud_id == selected.cloud_id: + table.move_cursor(row=index) + break + + self._show_project(self.selected_project) + + @property + def selected_project(self) -> Optional[UIProject]: + table = self.query_one(ProjectTable) + if not self._visible or table.cursor_row < 0 or table.cursor_row >= len(self._visible): + return None + return self._visible[table.cursor_row] + + @on(DataTable.RowHighlighted) + def _row_highlighted(self) -> None: + self._show_project(self.selected_project) + + @on(DataTable.RowSelected) + def _row_selected(self) -> None: + self.query_one(ActionList).focus() + + def _show_project(self, project: Optional[UIProject]) -> None: + details = self.query_one("#details", Static) + actions = self.query_one(ActionList) + actions.clear_options() + + if project is None: + query = self.query_one("#filter", Input).value + message = f"No projects match '{query}'." if self._projects and query else "No projects found. Create one with `lean project-create`." + details.update(Text(message, style="dim")) + return + + grid = Table.grid(padding=(0, 2)) + grid.add_column(style="bold") + grid.add_column() + grid.add_row("Name", project.name) + grid.add_row("Location", project.location) + grid.add_row("Language", project.language or "unknown") + grid.add_row("Cloud id", str(project.cloud_id) if project.is_cloud else "not in the cloud") + grid.add_row("Path", str(project.path) if project.is_local else "not pulled") + if project.modified is not None: + grid.add_row("Modified", project.modified.strftime("%Y-%m-%d %H:%M")) + if project.description: + grid.add_row("Description", project.description) + details.update(grid) + + for action in ACTIONS: + reason = action.unavailable_reason(project, self._logged_in) + prompt = Text.assemble((f" {action.key} ", "bold reverse"), f" {action.label}") + if reason is not None: + prompt.append(f" ({reason})", style="dim") + actions.add_option(Option(prompt, id=action.key, disabled=reason is not None)) + + available = [i for i, a in enumerate(ACTIONS) if a.unavailable_reason(project, self._logged_in) is None] + if available: + actions.highlighted = available[0] + + # Navigation + + def action_focus_filter(self) -> None: + self.query_one("#filter", Input).focus() + + def action_back(self) -> None: + filter_input = self.query_one("#filter", Input) + if filter_input.has_focus and filter_input.value: + filter_input.value = "" + self.query_one(ProjectTable).focus() + + # Running commands + + @on(OptionList.OptionSelected) + def _action_selected(self, event: OptionList.OptionSelected) -> None: + self.action_run_action(event.option.id) + + def action_run_action(self, key: str) -> None: + project = self.selected_project + action = next(a for a in ACTIONS if a.key == key) + if project is None: + return + + reason = action.unavailable_reason(project, self._logged_in) + if reason is not None: + self.notify(f"Cannot {action.label.lower()} '{project.name}': {reason}", severity="warning") + return + + if self._process is not None: + self.notify("A command is already running, press x to stop it", severity="warning") + return + + args = action.build_args(project) + + def on_confirm(confirmed: Optional[bool]) -> None: + if confirmed: + self._run_command(action, args) + + self.push_screen(ConfirmScreen(format_command(args)), on_confirm) + + @work(thread=True, group="command") + def _run_command(self, action: ProjectAction, args: List[str]) -> None: + process = CommandProcess(args, self._root) + self._process = process + self.call_from_thread(self._command_started, args) + + output = self.query_one("#output", RichLog) + try: + exit_code = process.run(lambda line: self.call_from_thread(output.write, Text.from_ansi(line))) + except Exception as exception: + self.call_from_thread(output.write, Text(f"Could not run the command: {exception}", style="red")) + exit_code = -1 + + self._process = None + self.call_from_thread(self._command_finished, exit_code, action.refreshes_projects) + + def _command_started(self, args: List[str]) -> None: + output = self.query_one("#output", RichLog) + output.clear() + output.write(Text(f"$ {format_command(args)}", style="bold cyan")) + output.border_title = f"Output: {format_command(args)}" + output.border_subtitle = "running, x to stop" + + def _command_finished(self, exit_code: int, refresh_projects: bool) -> None: + output = self.query_one("#output", RichLog) + output.border_subtitle = "done" if exit_code == 0 else f"failed (exit code {exit_code})" + output.write(Text(output.border_subtitle, style="green" if exit_code == 0 else "red")) + if refresh_projects: + self.action_refresh() + + def action_stop_command(self) -> None: + if self._process is None: + return + self._process.stop() + self.query_one("#output", RichLog).border_subtitle = "stopping, x again to force" + + async def action_quit(self) -> None: + if self._process is not None: + self._process.stop() + self.exit() diff --git a/lean/ui/projects.py b/lean/ui/projects.py new file mode 100644 index 000000000..f5d3c0022 --- /dev/null +++ b/lean/ui/projects.py @@ -0,0 +1,147 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 dataclasses import dataclass +from datetime import datetime +from os import walk +from pathlib import Path +from typing import List, Optional + +from lean.constants import PROJECT_CONFIG_FILE_NAME +from lean.models.api import QCProject + +LANGUAGE_LABELS = {"Python": "Py", "CSharp": "C#", "Py": "Py", "C#": "C#"} + + +@dataclass +class UIProject: + """A project as shown in the UI, which may exist locally, in the cloud, or both.""" + + name: str + path: Optional[Path] = None + cloud_id: Optional[int] = None + language: str = "" + description: str = "" + modified: Optional[datetime] = None + + @property + def is_local(self) -> bool: + return self.path is not None + + @property + def is_cloud(self) -> bool: + return self.cloud_id is not None + + @property + def is_library(self) -> bool: + return self.name.split("/")[0] == "Library" + + @property + def location(self) -> str: + if self.is_local and self.is_cloud: + return "synced" + return "local" if self.is_local else "cloud" + + +def discover_local_projects(root: Path, skip_dirs: Optional[List[Path]] = None) -> List[UIProject]: + """Finds all projects under the CLI root directory. + + A project is a directory containing a project config file. Project directories are not searched further, + so backtest and live output folders inside a project are never mistaken for projects. + + :param root: the CLI root directory, the one containing the Lean config file + :param skip_dirs: directories not to search, like the data directory + :return: the local projects, sorted by name + """ + from lean.container import container + + skip = {str(p.resolve()) for p in (skip_dirs or [])} + projects = [] + + for current, dirs, files in walk(root): + current_path = Path(current) + if PROJECT_CONFIG_FILE_NAME in files and current_path != root: + config = container.project_config_manager.get_project_config(current_path) + projects.append(UIProject( + name=current_path.relative_to(root).as_posix(), + path=current_path, + cloud_id=config.get("cloud-id"), + language=LANGUAGE_LABELS.get(config.get("algorithm-language", ""), ""), + description=config.get("description", ""), + modified=datetime.fromtimestamp(current_path.stat().st_mtime) + )) + dirs.clear() + continue + + dirs[:] = sorted(d for d in dirs + if not d.startswith(".") and str((current_path / d).resolve()) not in skip) + + return sorted(projects, key=lambda p: p.name.casefold()) + + +def merge_cloud_projects(local_projects: List[UIProject], cloud_projects: List[QCProject]) -> List[UIProject]: + """Combines local projects with the projects in the cloud. + + Local projects are matched to cloud projects by their cloud id, cloud projects without a local copy are added. + + :param local_projects: the projects found on disk + :param cloud_projects: the projects of the working organization in the cloud + :return: all projects, sorted by name + """ + by_cloud_id = {p.cloud_id: p for p in local_projects if p.cloud_id is not None} + merged = list(local_projects) + + for cloud_project in cloud_projects: + local_project = by_cloud_id.get(cloud_project.projectId) + if local_project is not None: + local_project.description = cloud_project.description or local_project.description + local_project.modified = cloud_project.modified + continue + + merged.append(UIProject( + name=cloud_project.name, + cloud_id=cloud_project.projectId, + language=LANGUAGE_LABELS.get(cloud_project.language.value, cloud_project.language.value), + description=cloud_project.description, + modified=cloud_project.modified + )) + + # Cloud projects whose id is stored locally but no longer exist are shown as local only + cloud_ids = {p.projectId for p in cloud_projects} + for project in merged: + if project.is_local and project.cloud_id not in cloud_ids: + project.cloud_id = None + + return sorted(merged, key=lambda p: p.name.casefold()) + + +def filter_projects(projects: List[UIProject], query: str) -> List[UIProject]: + """Returns the projects whose name contains every word of the query, ignoring case.""" + words = query.casefold().split() + return [p for p in projects if all(w in p.name.casefold() for w in words)] + + +def is_logged_in() -> bool: + """Returns whether credentials are stored, without contacting the API.""" + from lean.container import container + + cli_config_manager = container.cli_config_manager + return cli_config_manager.user_id.get_value() is not None and cli_config_manager.api_token.get_value() is not None + + +def fetch_cloud_projects() -> List[QCProject]: + """Returns the projects of the working organization in the cloud. Makes a blocking API request.""" + from lean.container import container + + organization_id = container.organization_manager.try_get_working_organization_id() + return container.api_client.projects.get_all(organization_id) diff --git a/setup.py b/setup.py index 8eae69504..649e8425b 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,8 @@ def get_stubs_version_range() -> str: "packaging", f"quantconnect-stubs{get_stubs_version_range()}", "cryptography>=41.0.4", + "textual>=1.0.0", + "textual-plotext>=1.0.0", ] setup( diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py new file mode 100644 index 000000000..f634ec837 --- /dev/null +++ b/tests/commands/test_ui.py @@ -0,0 +1,228 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +import asyncio +import json +from pathlib import Path +from unittest import mock + +from click.testing import CliRunner + +from lean.commands import lean +from lean.container import container +from lean.ui import app as ui_app +from lean.ui.actions import ACTIONS, format_command +from lean.ui.app import ConfirmScreen, LeanApp, ProjectTable +from lean.ui.projects import UIProject, discover_local_projects, filter_projects, merge_cloud_projects +from tests.test_helpers import create_api_project, create_fake_lean_cli_directory + + +def _discover() -> list: + return discover_local_projects(Path.cwd(), [Path.cwd() / "data"]) + + +def _action(key: str): + return next(a for a in ACTIONS if a.key == key) + + +def test_discover_local_projects_finds_projects_and_libraries() -> None: + create_fake_lean_cli_directory() + + names = [p.name for p in _discover()] + + assert names == ["CSharp Project", "Library/CSharp Library", "Library/Python Library", "Python Project"] + + +def test_discover_local_projects_skips_project_output_and_data_directories() -> None: + create_fake_lean_cli_directory() + for path in [Path.cwd() / "Python Project" / "backtests" / "2026-01-01_00-00-00" / "config.json", + Path.cwd() / "data" / "some-dataset" / "config.json", + Path.cwd() / ".hidden" / "config.json"]: + path.parent.mkdir(parents=True) + path.write_text("{}") + + names = [p.name for p in _discover()] + + assert names == ["CSharp Project", "Library/CSharp Library", "Library/Python Library", "Python Project"] + + +def test_discover_local_projects_reads_project_config() -> None: + create_fake_lean_cli_directory() + config_path = Path.cwd() / "Python Project" / "config.json" + config_path.write_text(json.dumps({"algorithm-language": "Python", "cloud-id": 7, "description": "Momentum"})) + + project = next(p for p in _discover() if p.name == "Python Project") + + assert project.language == "Py" + assert project.cloud_id == 7 + assert project.description == "Momentum" + assert project.path == Path.cwd() / "Python Project" + + +def test_merge_cloud_projects_matches_by_cloud_id_and_adds_cloud_only_projects() -> None: + local = [UIProject("Synced", Path("Synced"), cloud_id=1), + UIProject("Local Only", Path("Local Only")), + UIProject("Deleted In Cloud", Path("Deleted In Cloud"), cloud_id=3)] + cloud = [create_api_project(1, "Synced Cloud Name"), create_api_project(2, "Cloud Only")] + + merged = {p.name: p for p in merge_cloud_projects(local, cloud)} + + assert list(merged) == ["Cloud Only", "Deleted In Cloud", "Local Only", "Synced"] + assert merged["Synced"].location == "synced" + assert merged["Local Only"].location == "local" + assert merged["Deleted In Cloud"].location == "local" + assert merged["Cloud Only"].location == "cloud" + assert merged["Cloud Only"].cloud_id == 2 + + +def test_filter_projects_matches_every_word_ignoring_case() -> None: + projects = [UIProject("Momentum Strategy"), UIProject("Mean Reversion"), UIProject("Library/Momentum Utils")] + + assert [p.name for p in filter_projects(projects, "momentum strat")] == ["Momentum Strategy"] + assert [p.name for p in filter_projects(projects, "MOMENTUM")] == ["Momentum Strategy", "Library/Momentum Utils"] + assert len(filter_projects(projects, "")) == 3 + + +def test_actions_are_unavailable_when_project_or_login_is_missing() -> None: + local = UIProject("Local", Path("Local")) + cloud = UIProject("Cloud", cloud_id=5) + + assert _action("b").unavailable_reason(local, logged_in=False) is None + assert _action("b").unavailable_reason(cloud, logged_in=True) == "pull it first" + assert _action("p").unavailable_reason(local, logged_in=False) == "run `lean login` first" + assert _action("u").unavailable_reason(local, logged_in=True) == "push it first" + assert _action("u").unavailable_reason(cloud, logged_in=True) is None + + +def test_action_commands() -> None: + local = UIProject("My Project", Path("My Project")) + cloud = UIProject("Cloud Project", cloud_id=5) + + assert format_command(_action("b").build_args(local)) == "lean backtest 'My Project'" + assert _action("c").build_args(local) == ["cloud", "backtest", "My Project", "--push"] + assert _action("c").build_args(cloud) == ["cloud", "backtest", "5"] + assert _action("u").build_args(cloud) == ["cloud", "pull", "--project", "5"] + + +def test_ui_requires_lean_config() -> None: + result = CliRunner().invoke(lean, ["ui"]) + + assert result.exit_code != 0 + assert "requires a Lean configuration file" in str(result.exception) + + +def _run_app(test) -> None: + """Runs a test coroutine against the app with the fake Lean CLI directory and no stored credentials.""" + create_fake_lean_cli_directory() + container.cli_config_manager.user_id.get_value = mock.Mock(return_value=None) + + async def run() -> None: + app = LeanApp(Path.cwd(), Path.cwd() / "data") + async with app.run_test(size=(140, 40)) as pilot: + await app.workers.wait_for_complete() + await pilot.pause() + await test(app, pilot) + + asyncio.run(run()) + + +def test_app_lists_local_projects_and_filters_them() -> None: + async def test(app: LeanApp, pilot) -> None: + table = app.query_one(ProjectTable) + assert table.row_count == 4 + + await pilot.press("slash", *"python") + assert table.row_count == 2 + assert [p.name for p in app._visible] == ["Library/Python Library", "Python Project"] + + await pilot.press("escape") + assert table.row_count == 4 + assert table.has_focus + + _run_app(test) + + +def test_app_moves_through_projects_with_the_keyboard() -> None: + async def test(app: LeanApp, pilot) -> None: + assert app.selected_project.name == "CSharp Project" + + await pilot.press("j", "j", "j") + assert app.selected_project.name == "Python Project" + + await pilot.press("k") + assert app.selected_project.name == "Library/Python Library" + + _run_app(test) + + +def test_app_asks_for_confirmation_before_running_a_command() -> None: + async def test(app: LeanApp, pilot) -> None: + with mock.patch.object(ui_app, "CommandProcess") as command_process: + await pilot.press("b") + assert isinstance(app.screen, ConfirmScreen) + assert app.screen._command == "lean backtest 'CSharp Project'" + + await pilot.press("n") + assert not isinstance(app.screen, ConfirmScreen) + command_process.assert_not_called() + + _run_app(test) + + +def test_app_runs_the_selected_action_and_shows_its_output() -> None: + async def test(app: LeanApp, pilot) -> None: + def fake_run(on_line): + on_line("Backtest finished") + return 0 + + with mock.patch.object(ui_app, "CommandProcess") as command_process: + command_process.return_value.run.side_effect = fake_run + + await pilot.press("enter") + assert app.focused.__class__.__name__ == "ActionList" + await pilot.press("enter", "y") + await app.workers.wait_for_complete() + await pilot.pause() + + command_process.assert_called_once_with(["backtest", "CSharp Project"], Path.cwd()) + output = app.query_one("#output") + lines = [line.text for line in output.lines] + assert "Backtest finished" in lines + assert output.border_subtitle == "done" + + _run_app(test) + + +def test_app_does_not_run_unavailable_actions() -> None: + async def test(app: LeanApp, pilot) -> None: + with mock.patch.object(ui_app, "CommandProcess") as command_process: + await pilot.press("p") + assert not isinstance(app.screen, ConfirmScreen) + command_process.assert_not_called() + + _run_app(test) + + +def test_command_process_runs_the_cli_as_a_child_process(fake_filesystem) -> None: + from lean import __version__ + from lean.ui.actions import CommandProcess + + lines = [] + fake_filesystem.pause() + try: + exit_code = CommandProcess(["--version"], Path(__file__).parent).run(lines.append) + finally: + fake_filesystem.resume() + + assert exit_code == 0 + assert lines == [f"lean {__version__}"] From a9c337c6f99d1fd3a6fe7c12b451e2805dd11b75 Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 16:17:37 -0400 Subject: [PATCH 2/8] feature: add backtest results screen to `lean ui` Pressing `v` on a project with local backtests opens a results screen modeled on the QuantConnect.com backtest page: a list of the project's backtests, the runtime statistics, equity against a scaled benchmark, drawdown, and tabs for statistics, orders, every other chart (series in a second unit go on a right-hand axis) and the log. Results are parsed from the result file LEAN writes, which uses the same chart, order and statistics format as the cloud API, so the parser can serve cloud backtests too. Running backtests report progress from their last equity point. Chart axes use round values and round times. Also fixes the projects screen being queried while another screen was on top, which failed when cloud projects finished loading behind a dialog, and stops project action keys from firing on other screens. Co-Authored-By: Claude Opus 5.5 (1M context) --- lean/ui/actions.py | 7 +- lean/ui/app.py | 67 +++++--- lean/ui/backtest_screen.py | 341 +++++++++++++++++++++++++++++++++++++ lean/ui/results.py | 228 +++++++++++++++++++++++++ lean/ui/ticks.py | 97 +++++++++++ tests/commands/test_ui.py | 96 +++++++++++ tests/test_ui_results.py | 177 +++++++++++++++++++ tests/test_ui_ticks.py | 77 +++++++++ 8 files changed, 1067 insertions(+), 23 deletions(-) create mode 100644 lean/ui/backtest_screen.py create mode 100644 lean/ui/results.py create mode 100644 lean/ui/ticks.py create mode 100644 tests/test_ui_results.py create mode 100644 tests/test_ui_ticks.py diff --git a/lean/ui/actions.py b/lean/ui/actions.py index fb4301bb0..3aa2aab1d 100644 --- a/lean/ui/actions.py +++ b/lean/ui/actions.py @@ -20,6 +20,7 @@ from typing import Callable, List, Optional from lean.ui.projects import UIProject +from lean.ui.results import list_local_backtests @dataclass(frozen=True) @@ -32,7 +33,8 @@ class ProjectAction: needs_cloud_project: bool needs_login: bool refreshes_projects: bool - build_args: Callable[[UIProject], List[str]] + build_args: Optional[Callable[[UIProject], List[str]]] + opens_results: bool = False def unavailable_reason(self, project: UIProject, logged_in: bool) -> Optional[str]: """Returns why the action cannot run on the project, or None if it can.""" @@ -42,6 +44,8 @@ def unavailable_reason(self, project: UIProject, logged_in: bool) -> Optional[st return "run `lean login` first" if self.needs_cloud_project and not project.is_cloud: return "push it first" + if self.opens_results and not list_local_backtests(project.path): + return "no local backtests yet" return None @@ -53,6 +57,7 @@ def _cloud_name(project: UIProject) -> str: ACTIONS = [ ProjectAction("b", "Backtest locally", True, False, False, False, lambda p: ["backtest", p.name]), + ProjectAction("v", "View backtest results", True, False, False, False, None, opens_results=True), ProjectAction("c", "Backtest in the cloud", False, False, True, True, lambda p: ["cloud", "backtest", p.name, "--push"] if p.is_local else ["cloud", "backtest", _cloud_name(p)]), diff --git a/lean/ui/app.py b/lean/ui/app.py index c5c966923..a38fc5923 100644 --- a/lean/ui/app.py +++ b/lean/ui/app.py @@ -25,8 +25,10 @@ from textual.widgets.option_list import Option from lean.ui.actions import ACTIONS, CommandProcess, ProjectAction, format_command +from lean.ui.backtest_screen import BacktestScreen from lean.ui.projects import (UIProject, discover_local_projects, fetch_cloud_projects, filter_projects, is_logged_in, merge_cloud_projects) +from lean.ui.results import list_local_backtests class ProjectTable(DataTable): @@ -105,6 +107,11 @@ def __init__(self, root: Path, data_dir: Optional[Path] = None) -> None: self._logged_in = False self._process: Optional[CommandProcess] = None + def _query(self, selector, expect_type=None): + """Queries the projects screen, which stays at the bottom of the stack while other screens are shown.""" + screen = self.screen_stack[0] + return screen.query_one(selector, expect_type) if expect_type is not None else screen.query_one(selector) + def compose(self) -> ComposeResult: yield Header() with Horizontal(): @@ -120,13 +127,13 @@ def compose(self) -> ComposeResult: def on_mount(self) -> None: self.sub_title = str(self._root) - table = self.query_one(ProjectTable) + table = self._query(ProjectTable) table.add_columns("Where", "Lang", "Project") table.border_title = "Projects" - self.query_one("#filter", Input).border_title = "Filter (/)" - self.query_one("#details", Static).border_title = "Project" - self.query_one(ActionList).border_title = "Actions" - self.query_one("#output", RichLog).border_title = "Output" + self._query("#filter", Input).border_title = "Filter (/)" + self._query("#details", Static).border_title = "Project" + self._query(ActionList).border_title = "Actions" + self._query("#output", RichLog).border_title = "Output" table.focus() self.action_refresh() @@ -160,7 +167,7 @@ def _load_projects(self) -> None: def _set_projects(self, projects: List[UIProject], logged_in: bool, status: str) -> None: self._projects = projects self._logged_in = logged_in - self.query_one(ProjectTable).border_subtitle = status + self._query(ProjectTable).border_subtitle = status self._apply_filter() # Filtering and selection @@ -171,13 +178,13 @@ def _filter_changed(self) -> None: @on(Input.Submitted, "#filter") def _filter_submitted(self) -> None: - self.query_one(ProjectTable).focus() + self._query(ProjectTable).focus() def _apply_filter(self) -> None: - table = self.query_one(ProjectTable) + table = self._query(ProjectTable) selected = self.selected_project - self._visible = filter_projects(self._projects, self.query_one("#filter", Input).value) + self._visible = filter_projects(self._projects, self._query("#filter", Input).value) table.clear() for index, project in enumerate(self._visible): @@ -197,7 +204,7 @@ def _apply_filter(self) -> None: @property def selected_project(self) -> Optional[UIProject]: - table = self.query_one(ProjectTable) + table = self._query(ProjectTable) if not self._visible or table.cursor_row < 0 or table.cursor_row >= len(self._visible): return None return self._visible[table.cursor_row] @@ -208,16 +215,19 @@ def _row_highlighted(self) -> None: @on(DataTable.RowSelected) def _row_selected(self) -> None: - self.query_one(ActionList).focus() + self._query(ActionList).focus() def _show_project(self, project: Optional[UIProject]) -> None: - details = self.query_one("#details", Static) - actions = self.query_one(ActionList) + details = self._query("#details", Static) + actions = self._query(ActionList) actions.clear_options() if project is None: - query = self.query_one("#filter", Input).value - message = f"No projects match '{query}'." if self._projects and query else "No projects found. Create one with `lean project-create`." + query = self._query("#filter", Input).value + if self._projects and query: + message = f"No projects match '{query}'." + else: + message = "No projects found. Create one with `lean project-create`." details.update(Text(message, style="dim")) return @@ -248,14 +258,20 @@ def _show_project(self, project: Optional[UIProject]) -> None: # Navigation + def check_action(self, action: str, parameters: tuple) -> Optional[bool]: + # The project actions belong to the projects screen, other screens use these keys for their own bindings + if action in ("run_action", "focus_filter", "refresh") and len(self.screen_stack) > 1: + return False + return True + def action_focus_filter(self) -> None: - self.query_one("#filter", Input).focus() + self._query("#filter", Input).focus() def action_back(self) -> None: - filter_input = self.query_one("#filter", Input) + filter_input = self._query("#filter", Input) if filter_input.has_focus and filter_input.value: filter_input.value = "" - self.query_one(ProjectTable).focus() + self._query(ProjectTable).focus() # Running commands @@ -274,6 +290,10 @@ def action_run_action(self, key: str) -> None: self.notify(f"Cannot {action.label.lower()} '{project.name}': {reason}", severity="warning") return + if action.opens_results: + self.push_screen(BacktestScreen(project.name, list_local_backtests(project.path))) + return + if self._process is not None: self.notify("A command is already running, press x to stop it", severity="warning") return @@ -292,7 +312,7 @@ def _run_command(self, action: ProjectAction, args: List[str]) -> None: self._process = process self.call_from_thread(self._command_started, args) - output = self.query_one("#output", RichLog) + output = self._query("#output", RichLog) try: exit_code = process.run(lambda line: self.call_from_thread(output.write, Text.from_ansi(line))) except Exception as exception: @@ -303,24 +323,27 @@ def _run_command(self, action: ProjectAction, args: List[str]) -> None: self.call_from_thread(self._command_finished, exit_code, action.refreshes_projects) def _command_started(self, args: List[str]) -> None: - output = self.query_one("#output", RichLog) + output = self._query("#output", RichLog) output.clear() output.write(Text(f"$ {format_command(args)}", style="bold cyan")) output.border_title = f"Output: {format_command(args)}" output.border_subtitle = "running, x to stop" def _command_finished(self, exit_code: int, refresh_projects: bool) -> None: - output = self.query_one("#output", RichLog) + output = self._query("#output", RichLog) output.border_subtitle = "done" if exit_code == 0 else f"failed (exit code {exit_code})" output.write(Text(output.border_subtitle, style="green" if exit_code == 0 else "red")) if refresh_projects: self.action_refresh() + else: + # A finished backtest can make the view action available + self._show_project(self.selected_project) def action_stop_command(self) -> None: if self._process is None: return self._process.stop() - self.query_one("#output", RichLog).border_subtitle = "stopping, x again to force" + self._query("#output", RichLog).border_subtitle = "stopping, x again to force" async def action_quit(self) -> None: if self._process is not None: diff --git a/lean/ui/backtest_screen.py b/lean/ui/backtest_screen.py new file mode 100644 index 000000000..4fccc9266 --- /dev/null +++ b/lean/ui/backtest_screen.py @@ -0,0 +1,341 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 List, NamedTuple, Optional, Sequence, Tuple + +from rich.table import Table +from rich.text import Text +from textual import on, work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Horizontal, Vertical, VerticalScroll +from textual.screen import Screen +from textual.widgets import DataTable, Footer, Header, OptionList, RichLog, Static, TabbedContent, TabPane +from textual.widgets.option_list import Option +from textual.worker import get_current_worker +from textual_plotext import PlotextPlot + +from lean.ui.results import BacktestResult, LocalBacktest, Series +from lean.ui.ticks import format_value_ticks, time_ticks, value_ticks + +# Most charts are a few hundred points, this keeps rendering fast for long high resolution backtests +MAX_PLOT_POINTS = 2000 + +# Runtime statistics shown above the charts, in the order the QuantConnect UI shows them +SUMMARY_STATISTICS = ["Equity", "Net Profit", "Return", "Probabilistic Sharpe Ratio", "Unrealized", "Holdings", + "Fees", "Volume"] + +# Statistics where a negative value is bad and a positive value is good +SIGNED_STATISTICS = {"Net Profit", "Return", "Unrealized", "Compounding Annual Return", "Sharpe Ratio", + "Sortino Ratio", "Alpha", "Expectancy", "Information Ratio", "Treynor Ratio"} + +SERIES_COLORS = ["cyan", "orange", "magenta", "green", "blue", "yellow", "red", "white"] + + +def _downsample(points: Sequence[Tuple[float, float]]) -> Sequence[Tuple[float, float]]: + if len(points) <= MAX_PLOT_POINTS: + return points + step = len(points) / MAX_PLOT_POINTS + sampled = [points[int(i * step)] for i in range(MAX_PLOT_POINTS)] + return sampled + [points[-1]] + + +def _style_statistic(name: str, value: str) -> Text: + if name in SIGNED_STATISTICS and value: + return Text(value, style="red" if value.lstrip("$").startswith("-") else "green") + return Text(value) + + +class Line(NamedTuple): + label: str + points: Sequence[Tuple[float, float]] + color: str + unit: str = "" + + +class TimeSeriesChart(PlotextPlot): + """A line chart of one or more series over time, with dates on the x axis and compact values on the y axis. + + Lines with the same unit as the first line use the left axis, lines in another unit use the right axis. + """ + + def __init__(self, *, fill: bool = False, id: Optional[str] = None) -> None: + super().__init__(id=id) + self._fill = fill + self._lines: List[Line] = [] + + def set_lines(self, lines: List[Line]) -> None: + self._lines = [line._replace(points=_downsample(line.points)) for line in lines if line.points] + self._replot() + + def on_resize(self) -> None: + self._replot() + + def _replot(self) -> None: + plt = self.plt + plt.clear_figure() + + if not self._lines: + plt.title("No data") + self.refresh() + return + + left_unit = self._lines[0].unit + sides = {"left": [line for line in self._lines if line.unit == left_unit], + "right": [line for line in self._lines if line.unit != left_unit]} + + for side, lines in sides.items(): + for line in lines: + plt.plot([x for x, _ in line.points], [y for _, y in line.points], + label=line.label if len(self._lines) > 1 else None, + color=line.color, marker="braille", fillx=self._fill, yside=side) + if lines: + y_values = [y for line in lines for _, y in line.points] + y_ticks, y_step = value_ticks(min(y_values), max(y_values), max(3, min(8, self.size.height // 2))) + units = {line.unit for line in lines} + plt.yticks(y_ticks, format_value_ticks(y_ticks, y_step, units.pop() if len(units) == 1 else ""), + yside=side) + + all_x = [x for line in self._lines for x, _ in line.points] + x_ticks, x_labels = time_ticks(min(all_x), max(all_x), max(2, self.size.width // 16)) + plt.xticks(x_ticks, x_labels) + + self.refresh() + + +class BacktestScreen(Screen): + """Shows the results of a project's local backtests, like the backtest results page on QuantConnect.com.""" + + BINDINGS = [ + Binding("escape", "app.pop_screen", "Back"), + Binding("r", "reload", "Reload"), + Binding("j", "next_backtest", show=False), + Binding("k", "previous_backtest", show=False), + Binding("1", "show_tab('statistics')", "Statistics"), + Binding("2", "show_tab('orders')", "Orders"), + Binding("3", "show_tab('charts')", "Charts"), + Binding("4", "show_tab('logs')", "Logs"), + ] + + DEFAULT_CSS = """ + BacktestScreen #bt-sidebar { width: 26; } + BacktestScreen #backtests, BacktestScreen #summary, BacktestScreen TimeSeriesChart, BacktestScreen TabbedContent { + border: round $primary-darken-2; + } + BacktestScreen #backtests:focus { border: round $accent; } + BacktestScreen #backtests { height: 1fr; } + BacktestScreen #summary { height: auto; padding: 0 1; } + BacktestScreen #equity { height: 1fr; min-height: 10; } + BacktestScreen #drawdown { height: 9; } + BacktestScreen TabbedContent { height: 1fr; min-height: 10; } + BacktestScreen #chart-names { width: 28; height: 1fr; } + BacktestScreen #chart { height: 1fr; border: none; } + BacktestScreen #statistics-scroll { height: 1fr; } + """ + + def __init__(self, project_name: str, backtests: List[LocalBacktest], selected: int = 0) -> None: + super().__init__() + self._project_name = project_name + self._backtests = backtests + self._selected = selected + self._result: Optional[BacktestResult] = None + + def compose(self) -> ComposeResult: + yield Header() + with Horizontal(): + with Vertical(id="bt-sidebar"): + yield OptionList(*[Option(b.name, id=str(i)) for i, b in enumerate(self._backtests)], id="backtests") + with Vertical(): + yield Static(id="summary") + yield TimeSeriesChart(id="equity") + yield TimeSeriesChart(fill=True, id="drawdown") + with TabbedContent(initial="statistics"): + with TabPane("Statistics", id="statistics"): + with VerticalScroll(id="statistics-scroll"): + yield Static(id="statistics-table") + with TabPane("Orders", id="orders"): + yield DataTable(id="orders-table", cursor_type="row", zebra_stripes=True) + with TabPane("Charts", id="charts"): + with Horizontal(): + yield OptionList(id="chart-names") + yield TimeSeriesChart(id="chart") + with TabPane("Logs", id="logs"): + yield RichLog(id="log", markup=False, min_width=20) + yield Footer() + + def on_mount(self) -> None: + self.sub_title = self._project_name + self.query_one("#backtests", OptionList).border_title = "Backtests" + self.query_one("#equity", TimeSeriesChart).border_title = "Strategy Equity" + self.query_one("#drawdown", TimeSeriesChart).border_title = "Drawdown" + self.query_one("#orders-table", DataTable).add_columns( + "ID", "Time (UTC)", "Symbol", "Type", "Side", "Quantity", "Price", "Value", "Status", "Tag") + + backtests = self.query_one("#backtests", OptionList) + backtests.highlighted = self._selected + backtests.focus() + self._load() + + # Selecting backtests + + @property + def backtest(self) -> LocalBacktest: + return self._backtests[self._selected] + + @on(OptionList.OptionHighlighted, "#backtests") + def _backtest_highlighted(self, event: OptionList.OptionHighlighted) -> None: + if event.option_index != self._selected: + self._selected = event.option_index + self._load() + + def action_next_backtest(self) -> None: + self.query_one("#backtests", OptionList).action_cursor_down() + + def action_previous_backtest(self) -> None: + self.query_one("#backtests", OptionList).action_cursor_up() + + def action_show_tab(self, tab: str) -> None: + self.query_one(TabbedContent).active = tab + + def action_reload(self) -> None: + self._load() + + # Loading results + + def _load(self) -> None: + self.query_one("#summary", Static).border_title = f"{self.backtest.name}: loading..." + self._load_result(self.backtest) + + @work(thread=True, exclusive=True, group="backtest") + def _load_result(self, backtest: LocalBacktest) -> None: + result = backtest.load() + log_lines = backtest.log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-2000:] \ + if backtest.log_file.is_file() else [] + if not get_current_worker().is_cancelled: + self.app.call_from_thread(self._show_result, backtest, result, log_lines) + + def _show_result(self, backtest: LocalBacktest, result: Optional[BacktestResult], log_lines: List[str]) -> None: + self._result = result + self._show_summary(backtest, result) + self._show_charts(result) + self._show_statistics(result) + self._show_orders(result) + self._show_chart_names(result) + log = self.query_one("#log", RichLog) + log.clear() + log.write("\n".join(log_lines) if log_lines else Text("No log file", style="dim")) + + def _show_summary(self, backtest: LocalBacktest, result: Optional[BacktestResult]) -> None: + summary = self.query_one("#summary", Static) + if result is None: + summary.border_title = backtest.name + summary.update(Text("No results found in this backtest's directory.", style="dim")) + return + + status = result.status or "Running" + progress = result.progress + if status != "Completed" and progress is not None: + status = f"{status} {progress:.0%}" + summary.border_title = f"{backtest.name}: {status}" + + grid = Table.grid(padding=(0, 3), expand=True) + names = [n for n in SUMMARY_STATISTICS if n in result.runtime_statistics] + for _ in names: + grid.add_column() + grid.add_row(*[Text("PSR" if n == "Probabilistic Sharpe Ratio" else n, style="dim") for n in names]) + grid.add_row(*[_style_statistic(n, result.runtime_statistics[n]) for n in names]) + if result.runtime_error: + grid.add_row(Text(result.runtime_error, style="bold red")) + summary.update(grid) + + def _show_charts(self, result: Optional[BacktestResult]) -> None: + equity_chart = self.query_one("#equity", TimeSeriesChart) + drawdown_chart = self.query_one("#drawdown", TimeSeriesChart) + equity = result.equity if result is not None else None + if equity is None: + equity_chart.set_lines([]) + drawdown_chart.set_lines([]) + return + + lines = [Line("Equity", equity.points, "cyan", "$")] + benchmark = result.benchmark + if benchmark is not None and benchmark.points and equity.points and benchmark.points[0][1]: + # Scale the benchmark to start at the starting equity so both are comparable + scale = equity.points[0][1] / benchmark.points[0][1] + lines.append(Line("Benchmark", [(x, y * scale) for x, y in benchmark.points], "gray", "$")) + equity_chart.set_lines(lines) + + drawdown = result.drawdown + drawdown_chart.set_lines([Line("Drawdown", drawdown.points, "red", "%")] if drawdown else []) + + def _show_statistics(self, result: Optional[BacktestResult]) -> None: + table = self.query_one("#statistics-table", Static) + if result is None or not (result.statistics or result.parameters): + table.update(Text("Statistics are available when the backtest completes.", style="dim")) + return + + grid = Table.grid(padding=(0, 2), expand=True) + for style in ["dim", None, "dim", None]: + grid.add_column(style=style) + items = list(result.statistics.items()) + half = (len(items) + 1) // 2 + for left, right in zip(items[:half], items[half:] + [("", "")]): + grid.add_row(left[0], _style_statistic(*left), right[0], _style_statistic(*right)) + if result.parameters: + grid.add_row("", "", "", "") + grid.add_row(Text("Parameters", style="bold"), "", "", "") + for name, value in result.parameters.items(): + grid.add_row(name, value, "", "") + table.update(grid) + + def _show_orders(self, result: Optional[BacktestResult]) -> None: + table = self.query_one("#orders-table", DataTable) + table.clear() + if result is None: + return + for order in result.orders: + table.add_row(str(order.id), + order.time.strftime("%Y-%m-%d %H:%M:%S") if order.time else "", + order.symbol, + order.type, + Text(order.direction, style="green" if order.direction == "Buy" else "red"), + f"{order.quantity:,.6g}", + f"{order.price:,.2f}", + f"{order.value:,.2f}", + order.status, + order.tag) + self.query_one(TabbedContent).get_tab("orders").label = f"Orders ({len(result.orders)})" + + def _show_chart_names(self, result: Optional[BacktestResult]) -> None: + names = self.query_one("#chart-names", OptionList) + previous = names.highlighted_option.id if names.highlighted_option is not None else None + names.clear_options() + if result is None or not result.charts: + self.query_one("#chart", TimeSeriesChart).set_lines([]) + return + names.add_options([Option(name, id=name) for name in sorted(result.charts)]) + chart_names = sorted(result.charts) + names.highlighted = chart_names.index(previous) if previous in chart_names else 0 + self._show_custom_chart(chart_names[names.highlighted]) + + @on(OptionList.OptionHighlighted, "#chart-names") + def _chart_highlighted(self, event: OptionList.OptionHighlighted) -> None: + self._show_custom_chart(event.option.id) + + def _show_custom_chart(self, name: str) -> None: + chart = self._result.charts.get(name) if self._result is not None else None + if chart is None: + return + series: List[Series] = [s for s in chart.series.values() if s.points] + self.query_one("#chart", TimeSeriesChart).set_lines( + [Line(s.name, s.points, SERIES_COLORS[i % len(SERIES_COLORS)], s.unit) for i, s in enumerate(series)]) diff --git a/lean/ui/results.py b/lean/ui/results.py new file mode 100644 index 000000000..0cf98412c --- /dev/null +++ b/lean/ui/results.py @@ -0,0 +1,228 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +# Reads LEAN backtest results. Local result files and the cloud API share the same JSON format for charts, +# statistics and orders, so both are parsed here. + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from json import loads +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +# LEAN enums, see Common/Orders/OrderTypes.cs and Common/Orders/OrderEnums.cs +ORDER_TYPES = ["Market", "Limit", "Stop Market", "Stop Limit", "Market On Open", "Market On Close", + "Option Exercise", "Limit If Touched", "Combo Market", "Combo Limit", "Combo Leg Limit", + "Trailing Stop"] +ORDER_STATUSES = {0: "New", 1: "Submitted", 2: "Partially Filled", 3: "Filled", 5: "Canceled", 6: "None", + 7: "Invalid", 8: "Cancel Pending", 9: "Update Submitted"} +ORDER_DIRECTIONS = ["Buy", "Sell", "Hold"] + +# The candle series type, whose points are [time, open, high, low, close], see Common/SeriesType.cs +CANDLE_SERIES_TYPE = 2 + +# Files written next to the result file that are not the result itself +_NON_RESULT_SUFFIXES = ("-order-events.json", "-summary.json") + + +@dataclass +class Series: + name: str + unit: str + series_type: int + points: List[Tuple[float, float]] + """(unix time, value) pairs; candle series are reduced to their closing value.""" + + +@dataclass +class Chart: + name: str + series: Dict[str, Series] + + +@dataclass +class Order: + id: int + time: Optional[datetime] + symbol: str + type: str + direction: str + quantity: float + price: float + value: float + status: str + tag: str + + +@dataclass +class BacktestResult: + charts: Dict[str, Chart] = field(default_factory=dict) + statistics: Dict[str, str] = field(default_factory=dict) + runtime_statistics: Dict[str, str] = field(default_factory=dict) + orders: List[Order] = field(default_factory=list) + status: str = "" + runtime_error: str = "" + start: Optional[datetime] = None + end: Optional[datetime] = None + parameters: Dict[str, str] = field(default_factory=dict) + + def series(self, chart: str, series: str) -> Optional[Series]: + found = self.charts.get(chart) + return found.series.get(series) if found is not None else None + + @property + def equity(self) -> Optional[Series]: + return self.series("Strategy Equity", "Equity") + + @property + def drawdown(self) -> Optional[Series]: + return self.series("Drawdown", "Equity Drawdown") + + @property + def benchmark(self) -> Optional[Series]: + return self.series("Benchmark", "Benchmark") + + @property + def progress(self) -> Optional[float]: + """How far the backtest got through its date range, between 0 and 1, based on the last equity point.""" + equity = self.equity + if self.status == "Completed": + return 1.0 + if equity is None or not equity.points or self.start is None or self.end is None: + return None + total = (self.end - self.start).total_seconds() + done = equity.points[-1][0] - self.start.timestamp() + return min(max(done / total, 0.0), 1.0) if total > 0 else None + + +def _parse_time(value: Any) -> Optional[datetime]: + if not value: + return None + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value, tz=timezone.utc) + try: + return datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError: + return None + + +def _parse_point(value: Any) -> Optional[Tuple[float, float]]: + """Parses a chart point, which is [time, value], [time, open, high, low, close] or {"x": time, "y": value}.""" + if isinstance(value, dict): + time, number = value.get("x"), value.get("y") + elif isinstance(value, list) and len(value) >= 2: + time, number = value[0], value[-1] + else: + return None + if time is None or number is None: + return None + return float(time), float(number) + + +def _parse_series(data: Dict[str, Any]) -> Series: + points = [p for p in (_parse_point(v) for v in data.get("values") or []) if p is not None] + return Series(name=data.get("name", ""), + unit=data.get("unit", ""), + series_type=data.get("seriesType", 0), + points=points) + + +def _parse_order(data: Dict[str, Any]) -> Order: + symbol = data.get("symbol") or {} + order_type = data.get("type", 0) + direction = data.get("direction", 0) + return Order(id=data.get("id", 0), + time=_parse_time(data.get("time")), + symbol=symbol.get("value", "") if isinstance(symbol, dict) else str(symbol), + type=ORDER_TYPES[order_type] if 0 <= order_type < len(ORDER_TYPES) else str(order_type), + direction=ORDER_DIRECTIONS[direction] if 0 <= direction < len(ORDER_DIRECTIONS) else str(direction), + quantity=data.get("quantity", 0.0), + price=data.get("price", 0.0), + value=data.get("value", 0.0), + status=ORDER_STATUSES.get(data.get("status"), str(data.get("status"))), + tag=data.get("tag") or "") + + +def parse_backtest_result(data: Dict[str, Any]) -> BacktestResult: + """Parses LEAN backtest results, as stored in the result file of a local backtest.""" + charts = {} + for name, chart in (data.get("charts") or {}).items(): + charts[name] = Chart(name=name, + series={n: _parse_series(s) for n, s in (chart.get("series") or {}).items()}) + + orders = data.get("orders") or {} + if isinstance(orders, dict): + orders = list(orders.values()) + + state = data.get("state") or {} + configuration = data.get("algorithmConfiguration") or {} + + return BacktestResult(charts=charts, + statistics=data.get("statistics") or {}, + runtime_statistics=data.get("runtimeStatistics") or {}, + orders=sorted((_parse_order(o) for o in orders), key=lambda o: o.id), + status=state.get("Status", ""), + runtime_error=state.get("RuntimeError", ""), + start=_parse_time(configuration.get("startDate")), + end=_parse_time(configuration.get("endDate")), + parameters=configuration.get("parameters") or {}) + + +@dataclass +class LocalBacktest: + """A backtest stored in a project's backtests directory.""" + + path: Path + + @property + def name(self) -> str: + return self.path.name + + @property + def result_file(self) -> Optional[Path]: + """The result file, which LEAN names after the backtest id and rewrites while the backtest runs.""" + config_file = self.path / "config" + if config_file.is_file(): + try: + result_file = self.path / f"{loads(config_file.read_text(encoding='utf-8'))['id']}.json" + if result_file.is_file(): + return result_file + except (ValueError, KeyError): + pass + + candidates = [f for f in self.path.glob("*.json") + if f.stem.lstrip("L-").isdigit() and not f.name.endswith(_NON_RESULT_SUFFIXES)] + return max(candidates, key=lambda f: f.stat().st_mtime) if candidates else None + + @property + def log_file(self) -> Path: + return self.path / "log.txt" + + def load(self) -> Optional[BacktestResult]: + """Reads the result file, returns None if there is none yet or it is being written.""" + result_file = self.result_file + if result_file is None: + return None + try: + return parse_backtest_result(loads(result_file.read_text(encoding="utf-8"))) + except ValueError: + return None + + +def list_local_backtests(project_dir: Path) -> List[LocalBacktest]: + """Returns the backtests of a project, newest first.""" + backtests_dir = project_dir / "backtests" + if not backtests_dir.is_dir(): + return [] + return sorted((LocalBacktest(d) for d in backtests_dir.iterdir() if d.is_dir()), + key=lambda b: b.name, reverse=True) diff --git a/lean/ui/ticks.py b/lean/ui/ticks.py new file mode 100644 index 000000000..cedc3d378 --- /dev/null +++ b/lean/ui/ticks.py @@ -0,0 +1,97 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +# Chooses chart axis ticks at round values and round times, the way charting libraries do + +from datetime import datetime, timezone +from math import ceil, floor, log10 +from typing import List, Tuple + +_MINUTE = 60 +_HOUR = 60 * _MINUTE +_DAY = 24 * _HOUR + +# (step, step is in months rather than seconds, label format) +_TIME_STEPS = [(s * _MINUTE, False, "%H:%M") for s in (1, 5, 15, 30)] + \ + [(s * _HOUR, False, "%m-%d %H:%M") for s in (1, 2, 4, 6, 12)] + \ + [(s * _DAY, False, "%Y-%m-%d") for s in (1, 2, 7, 14)] + \ + [(s, True, "%Y-%m") for s in (1, 2, 3, 6)] + \ + [(s * 12, True, "%Y") for s in (1, 2, 5, 10, 20, 50)] + + +def value_ticks(low: float, high: float, max_ticks: int) -> Tuple[List[float], float]: + """Returns ticks at round values (1, 2, 2.5 or 5 times a power of ten) covering the range, and their step.""" + if high <= low: + return [low], 0.0 + + # Use the smallest round step that keeps the number of ticks within the limit + magnitude = 10 ** floor(log10((high - low) / max(1, max_ticks))) + for step in (m * magnitude * scale for scale in (1, 10, 100) for m in (1, 2, 2.5, 5)): + first = ceil(low / step - 1e-9) + last = floor(high / step + 1e-9) + if 0 < last - first + 1 <= max_ticks: + return [0.0 if i == 0 else i * step for i in range(first, last + 1)], step + + # The range is narrower than a round step, so label its ends instead + return [low, high], high - low + + +def format_value_ticks(ticks: List[float], step: float, unit: str = "") -> List[str]: + """Formats ticks compactly with the precision their step needs, like $1.25M or 12.5%.""" + largest = max(abs(t) for t in ticks) + divisor, suffix = next(((d, s) for d, s in [(1e9, "B"), (1e6, "M"), (1e3, "k")] if largest >= d), (1, "")) + # The fewest decimals that show the step exactly, so 0.25 steps get 2 decimals and 5 steps get none + scaled_step = step / divisor + decimals = next((d for d in range(7) if abs(round(scaled_step, d) - scaled_step) < scaled_step * 1e-6), 2) \ + if scaled_step > 0 else 2 + + labels = [] + for tick in ticks: + text = f"{abs(tick) / divisor:,.{decimals}f}{suffix}" + sign = "-" if tick < 0 else "" + labels.append(f"{sign}${text}" if unit == "$" else f"{sign}{text}%" if unit == "%" else f"{sign}{text}") + return labels + + +def _add_months(time: datetime, months: int) -> datetime: + month_index = time.year * 12 + time.month - 1 + months + return time.replace(year=month_index // 12, month=month_index % 12 + 1) + + +def time_ticks(low: float, high: float, max_ticks: int) -> Tuple[List[float], List[str]]: + """Returns ticks at round UTC times (whole hours, days, months, years) covering the range, and their labels.""" + if high <= low: + return [low], [datetime.fromtimestamp(low, tz=timezone.utc).strftime("%Y-%m-%d")] + + for step, in_months, label_format in _TIME_STEPS: + if in_months: + start = datetime.fromtimestamp(low, tz=timezone.utc).replace(day=1, hour=0, minute=0, second=0, + microsecond=0) + start = _add_months(start, (-(start.year * 12 + start.month - 1)) % step) + if start.timestamp() < low: + start = _add_months(start, step) + times = [] + while start.timestamp() <= high and len(times) <= max_ticks: + times.append(start.timestamp()) + start = _add_months(start, step) + else: + first = ceil(low / step) * step + count = int((high - first) // step) + 1 + if count > max_ticks: + continue + times = [first + i * step for i in range(count)] + + if 0 < len(times) <= max_ticks: + return times, [datetime.fromtimestamp(t, tz=timezone.utc).strftime(label_format) for t in times] + + return [low, high], [datetime.fromtimestamp(t, tz=timezone.utc).strftime("%Y") for t in (low, high)] diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index f634ec837..7bd680cba 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -17,14 +17,17 @@ from unittest import mock from click.testing import CliRunner +from textual.widgets import TabbedContent from lean.commands import lean from lean.container import container from lean.ui import app as ui_app from lean.ui.actions import ACTIONS, format_command from lean.ui.app import ConfirmScreen, LeanApp, ProjectTable +from lean.ui.backtest_screen import BacktestScreen from lean.ui.projects import UIProject, discover_local_projects, filter_projects, merge_cloud_projects from tests.test_helpers import create_api_project, create_fake_lean_cli_directory +from tests.test_ui_results import create_local_backtest, create_result def _discover() -> list: @@ -226,3 +229,96 @@ def test_command_process_runs_the_cli_as_a_child_process(fake_filesystem) -> Non assert exit_code == 0 assert lines == [f"lean {__version__}"] + + +def test_view_results_is_unavailable_without_local_backtests() -> None: + async def test(app: LeanApp, pilot) -> None: + await pilot.press("v") + assert not isinstance(app.screen, BacktestScreen) + assert _action("v").unavailable_reason(app.selected_project, logged_in=False) == "no local backtests yet" + + _run_app(test) + + +def test_app_opens_backtest_results_with_charts_statistics_and_orders() -> None: + async def test(app: LeanApp, pilot) -> None: + create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", create_result()) + + await pilot.press("v") + await app.workers.wait_for_complete() + await pilot.pause() + + screen = app.screen + assert isinstance(screen, BacktestScreen) + assert screen.query_one("#summary").border_title == "2026-10-08_16-00-57: Completed" + assert screen.query_one("#equity")._lines[0].label == "Equity" + assert screen.query_one("#equity")._lines[1].label == "Benchmark" + assert screen.query_one("#orders-table").row_count == 2 + + await pilot.press("3") + assert screen.query_one(TabbedContent).active == "charts" + assert screen.query_one("#chart-names").option_count == 4 + + await pilot.press("escape") + assert not isinstance(app.screen, BacktestScreen) + + _run_app(test) + + +def test_backtest_screen_switches_between_backtests() -> None: + async def test(app: LeanApp, pilot) -> None: + project_dir = Path.cwd() / "CSharp Project" + create_local_backtest(project_dir, "2026-10-08_16-00-57", create_result()) + create_local_backtest(project_dir, "2026-10-09_09-30-00", create_result(status="Running")) + + await pilot.press("v") + await app.workers.wait_for_complete() + await pilot.pause() + assert app.screen.query_one("#summary").border_title.startswith("2026-10-09_09-30-00: Running") + + await pilot.press("j") + await app.workers.wait_for_complete() + await pilot.pause() + assert app.screen.query_one("#summary").border_title == "2026-10-08_16-00-57: Completed" + + _run_app(test) + + +def test_backtest_screen_shows_a_message_for_backtests_without_results() -> None: + async def test(app: LeanApp, pilot) -> None: + (Path.cwd() / "CSharp Project" / "backtests" / "2026-10-08_16-00-57").mkdir(parents=True) + + await pilot.press("v") + await app.workers.wait_for_complete() + await pilot.pause() + + assert "No results found" in str(app.screen.query_one("#summary").render()) + + _run_app(test) + + +def test_project_keys_do_not_run_actions_on_other_screens() -> None: + async def test(app: LeanApp, pilot) -> None: + create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", create_result()) + await pilot.press("v") + await pilot.pause() + + with mock.patch.object(ui_app, "CommandProcess") as command_process: + await pilot.press("b") + assert isinstance(app.screen, BacktestScreen) + command_process.assert_not_called() + + _run_app(test) + + +def test_projects_can_update_while_another_screen_is_shown() -> None: + async def test(app: LeanApp, pilot) -> None: + await pilot.press("b") + assert isinstance(app.screen, ConfirmScreen) + + app._set_projects(app._projects[:1], logged_in=False, status="updated") + await pilot.pause() + + assert app.screen_stack[0].query_one(ProjectTable).row_count == 1 + + _run_app(test) diff --git a/tests/test_ui_results.py b/tests/test_ui_results.py new file mode 100644 index 000000000..d9e4344ee --- /dev/null +++ b/tests/test_ui_results.py @@ -0,0 +1,177 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict + +from lean.ui.results import LocalBacktest, list_local_backtests, parse_backtest_result + +# 2013-10-07 04:00 UTC to 2013-10-11 20:00 UTC, like the sample data backtests +START = 1381118400 +END = 1381521600 + + +def create_result(status: str = "Completed", equity_points: int = 5) -> Dict[str, Any]: + """Returns backtest results in the format LEAN writes them, trimmed to a few points.""" + step = (END - START) / max(1, equity_points - 1) + equity = [[START + i * step, 100000 + i, 100000 + i + 5, 100000 + i - 5, 100000 + i * 10] + for i in range(equity_points)] + + return { + "charts": { + "Strategy Equity": {"name": "Strategy Equity", "chartType": 0, "series": { + "Equity": {"name": "Equity", "unit": "$", "index": 0, "seriesType": 2, "values": equity}, + "Return": {"name": "Return", "unit": "%", "index": 1, "seriesType": 3, + "values": [[START, 0.0], [END, 0.4]]}, + }}, + "Drawdown": {"name": "Drawdown", "chartType": 0, "series": { + "Equity Drawdown": {"name": "Equity Drawdown", "unit": "%", "seriesType": 0, + "values": [[START, 0.0], [END, -0.34]]}, + }}, + "Benchmark": {"name": "Benchmark", "chartType": 0, "series": { + "Benchmark": {"name": "Benchmark", "unit": "$", "seriesType": 0, + "values": [[START, 146.0], [END, 147.0]]}, + }}, + "Indicators": {"name": "Indicators", "chartType": 0, "series": { + "Fast EMA": {"name": "Fast EMA", "unit": "$", "seriesType": 0, + "values": [[START, 145.0], [START + 60, None], {"x": END, "y": 146.0}]}, + }}, + }, + "orders": { + "2": {"type": 0, "id": 2, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, + "price": 145.34, "time": "2013-10-07T14:46:00Z", "quantity": -686.0, "status": 3, + "tag": "Liquidated", "direction": 1, "value": -99705.8}, + "1": {"type": 0, "id": 1, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, + "price": 145.33, "time": "2013-10-07T14:10:00Z", "quantity": 686.0, "status": 3, + "tag": "", "direction": 0, "value": 99693.94}, + }, + "statistics": {"Total Orders": "2", "Net Profit": "-0.160%", "Sharpe Ratio": "1.028"}, + "runtimeStatistics": {"Equity": "$99,839.57", "Net Profit": "$-294.86", "Return": "-0.16 %"}, + "state": {"Status": status, "RuntimeError": ""}, + "algorithmConfiguration": {"startDate": "2013-10-07T00:00:00Z", "endDate": "2013-10-11T23:59:59Z", + "parameters": {"delay-ms": "30"}}, + } + + +def create_local_backtest(project_dir: Path, name: str, result: Dict[str, Any], backtest_id: int = 1121419604) -> Path: + """Writes a backtest output directory the way `lean backtest` leaves it.""" + output_dir = project_dir / "backtests" / name + output_dir.mkdir(parents=True) + (output_dir / "config").write_text(json.dumps({"id": backtest_id, "container": "lean_cli_abc"})) + (output_dir / f"{backtest_id}.json").write_text(json.dumps(result)) + (output_dir / f"{backtest_id}-order-events.json").write_text("[]") + (output_dir / "log.txt").write_text("2026-10-08T20:01:00Z TRACE:: Engine.Run(): start\n") + return output_dir + + +def test_parse_backtest_result_reads_charts_and_reduces_candles_to_closes() -> None: + result = parse_backtest_result(create_result()) + + assert result.equity.unit == "$" + assert result.equity.points[0] == (START, 100000) + assert result.equity.points[-1] == (END, 100040) + assert result.drawdown.points == [(START, 0.0), (END, -0.34)] + assert result.benchmark.points[0] == (START, 146.0) + + +def test_parse_backtest_result_skips_empty_points_and_reads_xy_points() -> None: + result = parse_backtest_result(create_result()) + + assert result.series("Indicators", "Fast EMA").points == [(START, 145.0), (END, 146.0)] + + +def test_parse_backtest_result_reads_orders_sorted_by_id_with_readable_enums() -> None: + orders = parse_backtest_result(create_result()).orders + + assert [o.id for o in orders] == [1, 2] + assert (orders[0].type, orders[0].direction, orders[0].status) == ("Market", "Buy", "Filled") + assert orders[1].direction == "Sell" + assert orders[1].tag == "Liquidated" + assert orders[0].time == datetime(2013, 10, 7, 14, 10, tzinfo=timezone.utc) + + +def test_parse_backtest_result_reads_statistics_state_and_configuration() -> None: + result = parse_backtest_result(create_result()) + + assert result.statistics["Sharpe Ratio"] == "1.028" + assert result.runtime_statistics["Equity"] == "$99,839.57" + assert result.status == "Completed" + assert result.parameters == {"delay-ms": "30"} + assert result.start == datetime(2013, 10, 7, tzinfo=timezone.utc) + + +def test_parse_backtest_result_reads_cloud_backtests_with_list_orders() -> None: + data = create_result() + data["orders"] = list(data["orders"].values()) + del data["state"] + + result = parse_backtest_result(data) + + assert [o.id for o in result.orders] == [1, 2] + assert result.status == "" + + +def test_progress_of_a_running_backtest_comes_from_its_last_equity_point() -> None: + data = create_result(status="Running", equity_points=5) + data["charts"]["Strategy Equity"]["series"]["Equity"]["values"] = \ + data["charts"]["Strategy Equity"]["series"]["Equity"]["values"][:3] + + progress = parse_backtest_result(data).progress + + assert 0.45 < progress < 0.55 + + +def test_progress_of_a_completed_backtest_is_one() -> None: + assert parse_backtest_result(create_result()).progress == 1.0 + + +def test_local_backtest_finds_its_result_file_from_its_config() -> None: + project_dir = Path.cwd() / "Project" + output_dir = create_local_backtest(project_dir, "2026-10-08_16-00-57", create_result()) + (output_dir / "9999-summary.json").write_text("{}") + + backtest = LocalBacktest(output_dir) + + assert backtest.result_file == output_dir / "1121419604.json" + assert backtest.load().statistics["Total Orders"] == "2" + + +def test_local_backtest_finds_its_result_file_without_a_config() -> None: + output_dir = create_local_backtest(Path.cwd() / "Project", "2026-10-08_16-00-57", create_result()) + (output_dir / "config").unlink() + + assert LocalBacktest(output_dir).result_file == output_dir / "1121419604.json" + + +def test_local_backtest_without_results_or_with_a_partially_written_file_loads_nothing() -> None: + output_dir = create_local_backtest(Path.cwd() / "Project", "2026-10-08_16-00-57", create_result()) + backtest = LocalBacktest(output_dir) + + (output_dir / "1121419604.json").write_text('{"charts": {"Strategy Eq') + assert backtest.load() is None + + (output_dir / "1121419604.json").unlink() + assert backtest.load() is None + + +def test_list_local_backtests_returns_newest_first() -> None: + project_dir = Path.cwd() / "Project" + for name in ["2026-10-08_16-00-57", "2026-10-09_09-30-00", "2026-01-01_00-00-00"]: + create_local_backtest(project_dir, name, create_result()) + + names = [b.name for b in list_local_backtests(project_dir)] + + assert names == ["2026-10-09_09-30-00", "2026-10-08_16-00-57", "2026-01-01_00-00-00"] + assert list_local_backtests(Path.cwd() / "Missing") == [] diff --git a/tests/test_ui_ticks.py b/tests/test_ui_ticks.py new file mode 100644 index 000000000..9b6ac3964 --- /dev/null +++ b/tests/test_ui_ticks.py @@ -0,0 +1,77 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 datetime import datetime, timezone + +import pytest + +from lean.ui.ticks import format_value_ticks, time_ticks, value_ticks + + +def _labels(low: float, high: float, max_ticks: int, unit: str = "") -> list: + ticks, step = value_ticks(low, high, max_ticks) + return format_value_ticks(ticks, step, unit) + + +@pytest.mark.parametrize("low,high,max_ticks,unit,expected", [ + (97_980, 100_800, 5, "$", ["$98k", "$99k", "$100k"]), + (100_000, 236_588, 5, "$", ["$100k", "$150k", "$200k"]), + (1.394e6, 6.435e6, 5, "$", ["$2M", "$3M", "$4M", "$5M", "$6M"]), + (-0.84, 0.82, 5, "%", ["-0.5%", "0.0%", "0.5%"]), + (-1.34, 0, 3, "%", ["-1.0%", "-0.5%", "0.0%"]), + (0, 1, 5, "", ["0.00", "0.25", "0.50", "0.75", "1.00"]), +]) +def test_value_ticks_are_round_and_formatted_with_unit(low, high, max_ticks, unit, expected) -> None: + assert _labels(low, high, max_ticks, unit) == expected + + +@pytest.mark.parametrize("low,high,max_ticks", [ + (0.11, 0.14, 2), (0.111, 0.119, 2), (99_839.1, 99_839.6, 3), (-5e-7, 3e-7, 3), (1, 1e9, 3), +]) +def test_value_ticks_are_never_empty_and_stay_within_the_range(low, high, max_ticks) -> None: + ticks, _ = value_ticks(low, high, max_ticks) + + assert 0 < len(ticks) <= max_ticks + assert all(low - 1e-12 <= t <= high + 1e-12 for t in ticks) + + +def test_value_ticks_of_a_flat_series() -> None: + assert value_ticks(5, 5, 4) == ([5], 0.0) + + +def _timestamp(*args) -> float: + return datetime(*args, tzinfo=timezone.utc).timestamp() + + +def test_time_ticks_use_days_for_a_week() -> None: + _, labels = time_ticks(_timestamp(2013, 10, 7, 4), _timestamp(2013, 10, 11, 20), 6) + + assert labels == ["2013-10-08", "2013-10-09", "2013-10-10", "2013-10-11"] + + +def test_time_ticks_use_hours_within_a_day() -> None: + _, labels = time_ticks(_timestamp(2013, 10, 7, 13, 30), _timestamp(2013, 10, 7, 18), 5) + + assert labels == ["10-07 14:00", "10-07 15:00", "10-07 16:00", "10-07 17:00", "10-07 18:00"] + + +def test_time_ticks_use_years_for_decades() -> None: + _, labels = time_ticks(_timestamp(2000, 1, 1), _timestamp(2026, 10, 7), 6) + + assert labels == ["2000", "2005", "2010", "2015", "2020", "2025"] + + +def test_time_ticks_use_months_for_a_year() -> None: + _, labels = time_ticks(_timestamp(2023, 1, 15), _timestamp(2023, 12, 20), 6) + + assert labels == ["2023-03", "2023-05", "2023-07", "2023-09", "2023-11"] From af20dc95276da2f79c9dd7a09df89012ccd4388c Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 16:42:59 -0400 Subject: [PATCH 3/8] feature: show cloud backtests in `lean ui` and open results when backtesting The results screen now shows a project's cloud backtests next to its local ones, newest first, read through the API: the backtest summary and its Strategy Equity chart, with orders (100 per request) and logs (200 per request) loaded when their tab is opened. Backtests that are still running are refreshed every 15 seconds in the cloud and every 10 seconds locally. `b` and `c` now open the results right away instead of asking to confirm. A local backtest is written to an output directory the UI chooses, so its results can be read while it runs; its progress comes from the date range in LEAN's log, since the result file only gets it at the end. A cloud backtest switches to the backtest as soon as `lean cloud backtest` prints its url. The command's output is the backtest's log while it runs. The results screen uses the full width: `[` and `]` step through backtests and `o` opens a filterable list of all of them. Only the Strategy Equity chart is shown. The latest logs action is replaced by the Logs tab. Co-Authored-By: Claude Opus 5.5 (1M context) --- lean/ui/actions.py | 48 +- lean/ui/app.py | 59 ++- lean/ui/backtest_screen.py | 881 +++++++++++++++++++++++-------------- lean/ui/results.py | 653 +++++++++++++++++---------- tests/commands/test_ui.py | 256 ++++++++--- tests/test_ui_results.py | 271 +++++++++--- 6 files changed, 1426 insertions(+), 742 deletions(-) diff --git a/lean/ui/actions.py b/lean/ui/actions.py index 3aa2aab1d..98fce327a 100644 --- a/lean/ui/actions.py +++ b/lean/ui/actions.py @@ -12,6 +12,7 @@ # limitations under the License. import os +import re import subprocess import sys from dataclasses import dataclass @@ -25,27 +26,31 @@ @dataclass(frozen=True) class ProjectAction: - """A Lean CLI command that can be run on a project from the UI.""" + """Something to do with a project from the UI, usually running a Lean CLI command.""" key: str label: str - needs_local: bool - needs_cloud_project: bool - needs_login: bool - refreshes_projects: bool - build_args: Optional[Callable[[UIProject], List[str]]] + build_args: Optional[Callable[[UIProject], List[str]]] = None + needs_local: bool = False + needs_cloud_project: bool = False + needs_login: bool = False + refreshes_projects: bool = False opens_results: bool = False + live: Optional[str] = None + """'local' or 'cloud' for backtests, which open their results right away instead of asking to confirm.""" def unavailable_reason(self, project: UIProject, logged_in: bool) -> Optional[str]: """Returns why the action cannot run on the project, or None if it can.""" + if self.opens_results: + if list_local_backtests(project.path) or (project.is_cloud and logged_in): + return None + return "run `lean login` first" if project.is_cloud else "no backtests yet" if self.needs_local and not project.is_local: return "pull it first" if self.needs_login and not logged_in: return "run `lean login` first" if self.needs_cloud_project and not project.is_cloud: return "push it first" - if self.opens_results and not list_local_backtests(project.path): - return "no local backtests yet" return None @@ -55,22 +60,23 @@ def _cloud_name(project: UIProject) -> str: ACTIONS = [ - ProjectAction("b", "Backtest locally", True, False, False, False, - lambda p: ["backtest", p.name]), - ProjectAction("v", "View backtest results", True, False, False, False, None, opens_results=True), - ProjectAction("c", "Backtest in the cloud", False, False, True, True, + ProjectAction("b", "Backtest locally", lambda p: ["backtest", p.name], needs_local=True, live="local"), + ProjectAction("c", "Backtest in the cloud", lambda p: ["cloud", "backtest", p.name, "--push"] if p.is_local - else ["cloud", "backtest", _cloud_name(p)]), - ProjectAction("l", "Show latest backtest logs", True, False, False, False, - lambda p: ["logs", "--backtest", "--project", p.name]), - ProjectAction("p", "Push to the cloud", True, False, True, True, - lambda p: ["cloud", "push", "--project", p.name]), - ProjectAction("u", "Pull from the cloud", False, True, True, True, - lambda p: ["cloud", "pull", "--project", _cloud_name(p)]), - ProjectAction("s", "Cloud live status", False, True, True, False, - lambda p: ["cloud", "status", _cloud_name(p)]), + else ["cloud", "backtest", _cloud_name(p)], + needs_login=True, refreshes_projects=True, live="cloud"), + ProjectAction("v", "View backtest results", opens_results=True), + ProjectAction("p", "Push to the cloud", lambda p: ["cloud", "push", "--project", p.name], + needs_local=True, needs_login=True, refreshes_projects=True), + ProjectAction("u", "Pull from the cloud", lambda p: ["cloud", "pull", "--project", _cloud_name(p)], + needs_cloud_project=True, needs_login=True, refreshes_projects=True), + ProjectAction("s", "Cloud live status", lambda p: ["cloud", "status", _cloud_name(p)], + needs_cloud_project=True, needs_login=True), ] +# `lean cloud backtest` prints the url of the backtest it creates, which holds the project and backtest ids +CLOUD_BACKTEST_URL = re.compile(r"quantconnect\.com/project/(\d+)/([0-9a-f]{32})") + def format_command(args: List[str]) -> str: """Formats arguments as the `lean` command a user would type.""" diff --git a/lean/ui/app.py b/lean/ui/app.py index a38fc5923..da8a9a9ee 100644 --- a/lean/ui/app.py +++ b/lean/ui/app.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime from pathlib import Path from typing import List, Optional @@ -24,11 +25,11 @@ from textual.widgets import DataTable, Footer, Header, Input, OptionList, RichLog, Static from textual.widgets.option_list import Option -from lean.ui.actions import ACTIONS, CommandProcess, ProjectAction, format_command -from lean.ui.backtest_screen import BacktestScreen +from lean.ui.actions import ACTIONS, CLOUD_BACKTEST_URL, CommandProcess, ProjectAction, format_command +from lean.ui.backtest_screen import BacktestScreen, LiveRun from lean.ui.projects import (UIProject, discover_local_projects, fetch_cloud_projects, filter_projects, is_logged_in, merge_cloud_projects) -from lean.ui.results import list_local_backtests +from lean.ui.results import BacktestSource, CloudBacktest, LocalBacktest, PendingCloudBacktest class ProjectTable(DataTable): @@ -106,6 +107,7 @@ def __init__(self, root: Path, data_dir: Optional[Path] = None) -> None: self._visible: List[UIProject] = [] self._logged_in = False self._process: Optional[CommandProcess] = None + self._live_screen: Optional[BacktestScreen] = None def _query(self, selector, expect_type=None): """Queries the projects screen, which stays at the bottom of the stack while other screens are shown.""" @@ -291,7 +293,7 @@ def action_run_action(self, key: str) -> None: return if action.opens_results: - self.push_screen(BacktestScreen(project.name, list_local_backtests(project.path))) + self.push_screen(BacktestScreen(project, self._logged_in)) return if self._process is not None: @@ -300,28 +302,53 @@ def action_run_action(self, key: str) -> None: args = action.build_args(project) + if action.live is not None: + self._start_backtest(action, project, args) + return + def on_confirm(confirmed: Optional[bool]) -> None: if confirmed: self._run_command(action, args) self.push_screen(ConfirmScreen(format_command(args)), on_confirm) + def _start_backtest(self, action: ProjectAction, project: UIProject, args: List[str]) -> None: + """Runs a backtest and opens its results, which fill in as the backtest runs.""" + source: BacktestSource + if action.live == "local": + # Choosing the output directory lets the results screen read it while the backtest runs + output_dir = project.path / "backtests" / datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + args = args + ["--output", str(output_dir)] + source = LocalBacktest(output_dir) + else: + # The backtest id is known once the command has pushed and compiled the project + source = PendingCloudBacktest() + + live = LiveRun(source, lines=[f"$ {format_command(args)}"]) + self._live_screen = BacktestScreen(project, self._logged_in, live) + self.push_screen(self._live_screen) + self._run_command(action, args) + @work(thread=True, group="command") def _run_command(self, action: ProjectAction, args: List[str]) -> None: process = CommandProcess(args, self._root) self._process = process self.call_from_thread(self._command_started, args) - output = self._query("#output", RichLog) try: - exit_code = process.run(lambda line: self.call_from_thread(output.write, Text.from_ansi(line))) + exit_code = process.run(lambda line: self.call_from_thread(self._command_output, line)) except Exception as exception: - self.call_from_thread(output.write, Text(f"Could not run the command: {exception}", style="red")) + self.call_from_thread(self._command_output, f"Could not run the command: {exception}") exit_code = -1 self._process = None self.call_from_thread(self._command_finished, exit_code, action.refreshes_projects) + @property + def _live_screen_shown(self) -> Optional[BacktestScreen]: + """The results screen of the running backtest, unless it was closed.""" + return self._live_screen if self._live_screen in self.screen_stack else None + def _command_started(self, args: List[str]) -> None: output = self._query("#output", RichLog) output.clear() @@ -329,10 +356,28 @@ def _command_started(self, args: List[str]) -> None: output.border_title = f"Output: {format_command(args)}" output.border_subtitle = "running, x to stop" + def _command_output(self, line: str) -> None: + self._query("#output", RichLog).write(Text.from_ansi(line)) + + live_screen = self._live_screen_shown + if live_screen is None: + return + live_screen.add_live_line(line) + match = CLOUD_BACKTEST_URL.search(line) + if match is not None and isinstance(live_screen.live_source, PendingCloudBacktest): + live_screen.set_live_source(CloudBacktest(int(match.group(1)), match.group(2))) + def _command_finished(self, exit_code: int, refresh_projects: bool) -> None: output = self._query("#output", RichLog) output.border_subtitle = "done" if exit_code == 0 else f"failed (exit code {exit_code})" output.write(Text(output.border_subtitle, style="green" if exit_code == 0 else "red")) + + live_screen = self._live_screen_shown + if live_screen is not None: + live_screen.add_live_line(output.border_subtitle) + live_screen.live_finished(exit_code) + self._live_screen = None + if refresh_projects: self.action_refresh() else: diff --git a/lean/ui/backtest_screen.py b/lean/ui/backtest_screen.py index 4fccc9266..eb646cac4 100644 --- a/lean/ui/backtest_screen.py +++ b/lean/ui/backtest_screen.py @@ -1,341 +1,540 @@ -# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. -# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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 List, NamedTuple, Optional, Sequence, Tuple - -from rich.table import Table -from rich.text import Text -from textual import on, work -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, Vertical, VerticalScroll -from textual.screen import Screen -from textual.widgets import DataTable, Footer, Header, OptionList, RichLog, Static, TabbedContent, TabPane -from textual.widgets.option_list import Option -from textual.worker import get_current_worker -from textual_plotext import PlotextPlot - -from lean.ui.results import BacktestResult, LocalBacktest, Series -from lean.ui.ticks import format_value_ticks, time_ticks, value_ticks - -# Most charts are a few hundred points, this keeps rendering fast for long high resolution backtests -MAX_PLOT_POINTS = 2000 - -# Runtime statistics shown above the charts, in the order the QuantConnect UI shows them -SUMMARY_STATISTICS = ["Equity", "Net Profit", "Return", "Probabilistic Sharpe Ratio", "Unrealized", "Holdings", - "Fees", "Volume"] - -# Statistics where a negative value is bad and a positive value is good -SIGNED_STATISTICS = {"Net Profit", "Return", "Unrealized", "Compounding Annual Return", "Sharpe Ratio", - "Sortino Ratio", "Alpha", "Expectancy", "Information Ratio", "Treynor Ratio"} - -SERIES_COLORS = ["cyan", "orange", "magenta", "green", "blue", "yellow", "red", "white"] - - -def _downsample(points: Sequence[Tuple[float, float]]) -> Sequence[Tuple[float, float]]: - if len(points) <= MAX_PLOT_POINTS: - return points - step = len(points) / MAX_PLOT_POINTS - sampled = [points[int(i * step)] for i in range(MAX_PLOT_POINTS)] - return sampled + [points[-1]] - - -def _style_statistic(name: str, value: str) -> Text: - if name in SIGNED_STATISTICS and value: - return Text(value, style="red" if value.lstrip("$").startswith("-") else "green") - return Text(value) - - -class Line(NamedTuple): - label: str - points: Sequence[Tuple[float, float]] - color: str - unit: str = "" - - -class TimeSeriesChart(PlotextPlot): - """A line chart of one or more series over time, with dates on the x axis and compact values on the y axis. - - Lines with the same unit as the first line use the left axis, lines in another unit use the right axis. - """ - - def __init__(self, *, fill: bool = False, id: Optional[str] = None) -> None: - super().__init__(id=id) - self._fill = fill - self._lines: List[Line] = [] - - def set_lines(self, lines: List[Line]) -> None: - self._lines = [line._replace(points=_downsample(line.points)) for line in lines if line.points] - self._replot() - - def on_resize(self) -> None: - self._replot() - - def _replot(self) -> None: - plt = self.plt - plt.clear_figure() - - if not self._lines: - plt.title("No data") - self.refresh() - return - - left_unit = self._lines[0].unit - sides = {"left": [line for line in self._lines if line.unit == left_unit], - "right": [line for line in self._lines if line.unit != left_unit]} - - for side, lines in sides.items(): - for line in lines: - plt.plot([x for x, _ in line.points], [y for _, y in line.points], - label=line.label if len(self._lines) > 1 else None, - color=line.color, marker="braille", fillx=self._fill, yside=side) - if lines: - y_values = [y for line in lines for _, y in line.points] - y_ticks, y_step = value_ticks(min(y_values), max(y_values), max(3, min(8, self.size.height // 2))) - units = {line.unit for line in lines} - plt.yticks(y_ticks, format_value_ticks(y_ticks, y_step, units.pop() if len(units) == 1 else ""), - yside=side) - - all_x = [x for line in self._lines for x, _ in line.points] - x_ticks, x_labels = time_ticks(min(all_x), max(all_x), max(2, self.size.width // 16)) - plt.xticks(x_ticks, x_labels) - - self.refresh() - - -class BacktestScreen(Screen): - """Shows the results of a project's local backtests, like the backtest results page on QuantConnect.com.""" - - BINDINGS = [ - Binding("escape", "app.pop_screen", "Back"), - Binding("r", "reload", "Reload"), - Binding("j", "next_backtest", show=False), - Binding("k", "previous_backtest", show=False), - Binding("1", "show_tab('statistics')", "Statistics"), - Binding("2", "show_tab('orders')", "Orders"), - Binding("3", "show_tab('charts')", "Charts"), - Binding("4", "show_tab('logs')", "Logs"), - ] - - DEFAULT_CSS = """ - BacktestScreen #bt-sidebar { width: 26; } - BacktestScreen #backtests, BacktestScreen #summary, BacktestScreen TimeSeriesChart, BacktestScreen TabbedContent { - border: round $primary-darken-2; - } - BacktestScreen #backtests:focus { border: round $accent; } - BacktestScreen #backtests { height: 1fr; } - BacktestScreen #summary { height: auto; padding: 0 1; } - BacktestScreen #equity { height: 1fr; min-height: 10; } - BacktestScreen #drawdown { height: 9; } - BacktestScreen TabbedContent { height: 1fr; min-height: 10; } - BacktestScreen #chart-names { width: 28; height: 1fr; } - BacktestScreen #chart { height: 1fr; border: none; } - BacktestScreen #statistics-scroll { height: 1fr; } - """ - - def __init__(self, project_name: str, backtests: List[LocalBacktest], selected: int = 0) -> None: - super().__init__() - self._project_name = project_name - self._backtests = backtests - self._selected = selected - self._result: Optional[BacktestResult] = None - - def compose(self) -> ComposeResult: - yield Header() - with Horizontal(): - with Vertical(id="bt-sidebar"): - yield OptionList(*[Option(b.name, id=str(i)) for i, b in enumerate(self._backtests)], id="backtests") - with Vertical(): - yield Static(id="summary") - yield TimeSeriesChart(id="equity") - yield TimeSeriesChart(fill=True, id="drawdown") - with TabbedContent(initial="statistics"): - with TabPane("Statistics", id="statistics"): - with VerticalScroll(id="statistics-scroll"): - yield Static(id="statistics-table") - with TabPane("Orders", id="orders"): - yield DataTable(id="orders-table", cursor_type="row", zebra_stripes=True) - with TabPane("Charts", id="charts"): - with Horizontal(): - yield OptionList(id="chart-names") - yield TimeSeriesChart(id="chart") - with TabPane("Logs", id="logs"): - yield RichLog(id="log", markup=False, min_width=20) - yield Footer() - - def on_mount(self) -> None: - self.sub_title = self._project_name - self.query_one("#backtests", OptionList).border_title = "Backtests" - self.query_one("#equity", TimeSeriesChart).border_title = "Strategy Equity" - self.query_one("#drawdown", TimeSeriesChart).border_title = "Drawdown" - self.query_one("#orders-table", DataTable).add_columns( - "ID", "Time (UTC)", "Symbol", "Type", "Side", "Quantity", "Price", "Value", "Status", "Tag") - - backtests = self.query_one("#backtests", OptionList) - backtests.highlighted = self._selected - backtests.focus() - self._load() - - # Selecting backtests - - @property - def backtest(self) -> LocalBacktest: - return self._backtests[self._selected] - - @on(OptionList.OptionHighlighted, "#backtests") - def _backtest_highlighted(self, event: OptionList.OptionHighlighted) -> None: - if event.option_index != self._selected: - self._selected = event.option_index - self._load() - - def action_next_backtest(self) -> None: - self.query_one("#backtests", OptionList).action_cursor_down() - - def action_previous_backtest(self) -> None: - self.query_one("#backtests", OptionList).action_cursor_up() - - def action_show_tab(self, tab: str) -> None: - self.query_one(TabbedContent).active = tab - - def action_reload(self) -> None: - self._load() - - # Loading results - - def _load(self) -> None: - self.query_one("#summary", Static).border_title = f"{self.backtest.name}: loading..." - self._load_result(self.backtest) - - @work(thread=True, exclusive=True, group="backtest") - def _load_result(self, backtest: LocalBacktest) -> None: - result = backtest.load() - log_lines = backtest.log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-2000:] \ - if backtest.log_file.is_file() else [] - if not get_current_worker().is_cancelled: - self.app.call_from_thread(self._show_result, backtest, result, log_lines) - - def _show_result(self, backtest: LocalBacktest, result: Optional[BacktestResult], log_lines: List[str]) -> None: - self._result = result - self._show_summary(backtest, result) - self._show_charts(result) - self._show_statistics(result) - self._show_orders(result) - self._show_chart_names(result) - log = self.query_one("#log", RichLog) - log.clear() - log.write("\n".join(log_lines) if log_lines else Text("No log file", style="dim")) - - def _show_summary(self, backtest: LocalBacktest, result: Optional[BacktestResult]) -> None: - summary = self.query_one("#summary", Static) - if result is None: - summary.border_title = backtest.name - summary.update(Text("No results found in this backtest's directory.", style="dim")) - return - - status = result.status or "Running" - progress = result.progress - if status != "Completed" and progress is not None: - status = f"{status} {progress:.0%}" - summary.border_title = f"{backtest.name}: {status}" - - grid = Table.grid(padding=(0, 3), expand=True) - names = [n for n in SUMMARY_STATISTICS if n in result.runtime_statistics] - for _ in names: - grid.add_column() - grid.add_row(*[Text("PSR" if n == "Probabilistic Sharpe Ratio" else n, style="dim") for n in names]) - grid.add_row(*[_style_statistic(n, result.runtime_statistics[n]) for n in names]) - if result.runtime_error: - grid.add_row(Text(result.runtime_error, style="bold red")) - summary.update(grid) - - def _show_charts(self, result: Optional[BacktestResult]) -> None: - equity_chart = self.query_one("#equity", TimeSeriesChart) - drawdown_chart = self.query_one("#drawdown", TimeSeriesChart) - equity = result.equity if result is not None else None - if equity is None: - equity_chart.set_lines([]) - drawdown_chart.set_lines([]) - return - - lines = [Line("Equity", equity.points, "cyan", "$")] - benchmark = result.benchmark - if benchmark is not None and benchmark.points and equity.points and benchmark.points[0][1]: - # Scale the benchmark to start at the starting equity so both are comparable - scale = equity.points[0][1] / benchmark.points[0][1] - lines.append(Line("Benchmark", [(x, y * scale) for x, y in benchmark.points], "gray", "$")) - equity_chart.set_lines(lines) - - drawdown = result.drawdown - drawdown_chart.set_lines([Line("Drawdown", drawdown.points, "red", "%")] if drawdown else []) - - def _show_statistics(self, result: Optional[BacktestResult]) -> None: - table = self.query_one("#statistics-table", Static) - if result is None or not (result.statistics or result.parameters): - table.update(Text("Statistics are available when the backtest completes.", style="dim")) - return - - grid = Table.grid(padding=(0, 2), expand=True) - for style in ["dim", None, "dim", None]: - grid.add_column(style=style) - items = list(result.statistics.items()) - half = (len(items) + 1) // 2 - for left, right in zip(items[:half], items[half:] + [("", "")]): - grid.add_row(left[0], _style_statistic(*left), right[0], _style_statistic(*right)) - if result.parameters: - grid.add_row("", "", "", "") - grid.add_row(Text("Parameters", style="bold"), "", "", "") - for name, value in result.parameters.items(): - grid.add_row(name, value, "", "") - table.update(grid) - - def _show_orders(self, result: Optional[BacktestResult]) -> None: - table = self.query_one("#orders-table", DataTable) - table.clear() - if result is None: - return - for order in result.orders: - table.add_row(str(order.id), - order.time.strftime("%Y-%m-%d %H:%M:%S") if order.time else "", - order.symbol, - order.type, - Text(order.direction, style="green" if order.direction == "Buy" else "red"), - f"{order.quantity:,.6g}", - f"{order.price:,.2f}", - f"{order.value:,.2f}", - order.status, - order.tag) - self.query_one(TabbedContent).get_tab("orders").label = f"Orders ({len(result.orders)})" - - def _show_chart_names(self, result: Optional[BacktestResult]) -> None: - names = self.query_one("#chart-names", OptionList) - previous = names.highlighted_option.id if names.highlighted_option is not None else None - names.clear_options() - if result is None or not result.charts: - self.query_one("#chart", TimeSeriesChart).set_lines([]) - return - names.add_options([Option(name, id=name) for name in sorted(result.charts)]) - chart_names = sorted(result.charts) - names.highlighted = chart_names.index(previous) if previous in chart_names else 0 - self._show_custom_chart(chart_names[names.highlighted]) - - @on(OptionList.OptionHighlighted, "#chart-names") - def _chart_highlighted(self, event: OptionList.OptionHighlighted) -> None: - self._show_custom_chart(event.option.id) - - def _show_custom_chart(self, name: str) -> None: - chart = self._result.charts.get(name) if self._result is not None else None - if chart is None: - return - series: List[Series] = [s for s in chart.series.values() if s.points] - self.query_one("#chart", TimeSeriesChart).set_lines( - [Line(s.name, s.points, SERIES_COLORS[i % len(SERIES_COLORS)], s.unit) for i, s in enumerate(series)]) +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 dataclasses import dataclass, field +from datetime import timezone +from typing import List, Optional, Sequence, Tuple + +from rich.table import Table +from rich.text import Text +from textual import on, work +from textual.app import ComposeResult +from textual.binding import Binding +from textual.containers import Vertical, VerticalScroll +from textual.events import Key +from textual.screen import ModalScreen, Screen +from textual.timer import Timer +from textual.widgets import DataTable, Footer, Header, Input, OptionList, RichLog, Static, TabbedContent, TabPane +from textual.widgets.option_list import Option +from textual.worker import get_current_worker +from textual_plotext import PlotextPlot + +from lean.ui.projects import UIProject +from lean.ui.results import (BacktestResult, BacktestSource, Order, list_cloud_backtests, list_local_backtests, + sort_newest_first) +from lean.ui.ticks import format_value_ticks, time_ticks, value_ticks + +# Most equity charts are a few hundred points, this keeps rendering fast for long high resolution backtests +MAX_PLOT_POINTS = 2000 + +# Runtime statistics shown above the chart, in the order the QuantConnect UI shows them +SUMMARY_STATISTICS = ["Equity", "Net Profit", "Return", "Probabilistic Sharpe Ratio", "Unrealized", "Holdings", + "Fees", "Volume"] + +# Statistics where a negative value is bad and a positive value is good +SIGNED_STATISTICS = {"Net Profit", "Return", "Unrealized", "Compounding Annual Return", "Sharpe Ratio", + "Sortino Ratio", "Alpha", "Expectancy", "Information Ratio", "Treynor Ratio"} + + +def _downsample(points: Sequence[Tuple[float, float]]) -> Sequence[Tuple[float, float]]: + if len(points) <= MAX_PLOT_POINTS: + return points + step = len(points) / MAX_PLOT_POINTS + return [points[int(i * step)] for i in range(MAX_PLOT_POINTS)] + [points[-1]] + + +def _style_statistic(name: str, value: str) -> Text: + if name in SIGNED_STATISTICS and value: + return Text(value, style="red" if value.lstrip("$").startswith("-") else "green") + return Text(value) + + +def _format_created(source: BacktestSource) -> str: + created = source.created + return created.astimezone().strftime("%Y-%m-%d %H:%M") if created is not None else "" + + +@dataclass +class LiveRun: + """A backtest the UI started, whose command output is shown as its log.""" + + source: BacktestSource + lines: List[str] = field(default_factory=list) + exit_code: Optional[int] = None + + +class EquityChart(PlotextPlot): + """The equity of a backtest over time, with dates on the x axis and dollar values on the y axis.""" + + def __init__(self, *, id: Optional[str] = None) -> None: + super().__init__(id=id) + self._points: Sequence[Tuple[float, float]] = [] + self._message = "No data" + + @property + def points(self) -> Sequence[Tuple[float, float]]: + return self._points + + def set_points(self, points: Sequence[Tuple[float, float]], message: str = "No data") -> None: + self._points = _downsample(points) + self._message = message + self._replot() + + def on_resize(self) -> None: + self._replot() + + def _replot(self) -> None: + plt = self.plt + plt.clear_figure() + + if not self._points: + plt.title(self._message) + self.refresh() + return + + xs = [x for x, _ in self._points] + ys = [y for _, y in self._points] + plt.plot(xs, ys, color="cyan", marker="braille") + + y_ticks, y_step = value_ticks(min(ys), max(ys), max(3, min(8, self.size.height // 2))) + plt.yticks(y_ticks, format_value_ticks(y_ticks, y_step, "$")) + x_ticks, x_labels = time_ticks(min(xs), max(xs), max(2, self.size.width // 16)) + plt.xticks(x_ticks, x_labels) + + self.refresh() + + +class BacktestPicker(ModalScreen[Optional[int]]): + """Lists a project's backtests, local and cloud, to pick one to show.""" + + BINDINGS = [Binding("escape", "dismiss(None)", "Cancel")] + + DEFAULT_CSS = """ + BacktestPicker { align: center middle; } + BacktestPicker > Vertical { width: 90; max-width: 95%; height: 80%; border: round $accent; background: $surface; } + BacktestPicker OptionList { height: 1fr; border: none; } + """ + + def __init__(self, backtests: List[BacktestSource], selected: int) -> None: + super().__init__() + self._backtests = backtests + self._selected = selected + + def compose(self) -> ComposeResult: + with Vertical(): + yield Input(placeholder="type to filter backtests") + yield OptionList() + + def on_mount(self) -> None: + self.query_one(Vertical).border_title = f"Backtests ({len(self._backtests)})" + self._fill("") + self.query_one(Input).focus() + + def _fill(self, query: str) -> None: + options = self.query_one(OptionList) + options.clear_options() + words = query.casefold().split() + for index, backtest in enumerate(self._backtests): + if all(w in f"{backtest.name} {backtest.location}".casefold() for w in words): + options.add_option(Option(Text.assemble( + (f"{backtest.location:<6}", "cyan" if backtest.location == "cloud" else "yellow"), " ", + (f"{_format_created(backtest):<17}", "dim"), " ", backtest.name), id=str(index))) + ids = [options.get_option_at_index(i).id for i in range(options.option_count)] + if ids: + options.highlighted = ids.index(str(self._selected)) if str(self._selected) in ids else 0 + + @on(Input.Changed) + def _filter_changed(self, event: Input.Changed) -> None: + self._fill(event.value) + + @on(Input.Submitted) + def _filter_submitted(self) -> None: + options = self.query_one(OptionList) + if options.highlighted is not None: + self.dismiss(int(options.get_option_at_index(options.highlighted).id)) + + def on_key(self, event: Key) -> None: + # The arrow keys move through the list while typing in the filter + if event.key in ("up", "down"): + options = self.query_one(OptionList) + if event.key == "up": + options.action_cursor_up() + else: + options.action_cursor_down() + event.stop() + + @on(OptionList.OptionSelected) + def _option_selected(self, event: OptionList.OptionSelected) -> None: + self.dismiss(int(event.option.id)) + + +class BacktestScreen(Screen): + """Shows a project's backtest results like the QuantConnect.com backtest page, refreshing while they change.""" + + BINDINGS = [ + Binding("escape", "app.pop_screen", "Back"), + Binding("o", "pick_backtest", "Backtests"), + Binding("left_square_bracket", "step_backtest(-1)", "Newer"), + Binding("right_square_bracket", "step_backtest(1)", "Older"), + Binding("r", "reload", "Reload"), + Binding("1", "show_tab('statistics')", "Statistics"), + Binding("2", "show_tab('orders')", "Orders"), + Binding("3", "show_tab('logs')", "Logs"), + ] + + DEFAULT_CSS = """ + BacktestScreen #summary, BacktestScreen EquityChart, BacktestScreen TabbedContent { + border: round $primary-darken-2; + } + BacktestScreen #summary { height: auto; padding: 0 1; } + BacktestScreen EquityChart { height: 1fr; min-height: 10; } + BacktestScreen TabbedContent { height: 1fr; min-height: 10; } + BacktestScreen #statistics-scroll { height: 1fr; } + """ + + def __init__(self, project: UIProject, logged_in: bool, live: Optional[LiveRun] = None) -> None: + super().__init__() + self._project = project + self._logged_in = logged_in + self._live = live + self._backtests: List[BacktestSource] = [live.source] if live is not None else [] + self._selected = 0 + self._result: Optional[BacktestResult] = None + self._orders_loaded_for: Optional[str] = None + self._log_loaded_for: Optional[str] = None + self._poll_timer: Optional[Timer] = None + + def compose(self) -> ComposeResult: + yield Header() + with Vertical(): + yield Static(id="summary") + yield EquityChart(id="equity") + with TabbedContent(initial="statistics"): + with TabPane("Statistics", id="statistics"): + with VerticalScroll(id="statistics-scroll"): + yield Static(id="statistics-table") + with TabPane("Orders", id="orders"): + yield DataTable(id="orders-table", cursor_type="row", zebra_stripes=True) + with TabPane("Logs", id="logs"): + yield RichLog(id="log", markup=False, min_width=20) + yield Footer() + + def on_mount(self) -> None: + self.sub_title = self._project.name + self.query_one("#equity", EquityChart).border_title = "Strategy Equity" + self.query_one("#orders-table", DataTable).add_columns( + "ID", "Time (UTC)", "Symbol", "Type", "Side", "Quantity", "Price", "Value", "Status", "Tag") + if self._live is not None: + # Show what the command prints while the backtest starts + self.query_one(TabbedContent).active = "logs" + self._show_backtest() + self._list_backtests() + + # The list of backtests + + @property + def backtest(self) -> Optional[BacktestSource]: + return self._backtests[self._selected] if self._backtests else None + + @property + def result(self) -> Optional[BacktestResult]: + return self._result + + @work(thread=True, exclusive=True, group="list") + def _list_backtests(self) -> None: + backtests: List[BacktestSource] = list(list_local_backtests(self._project.path)) + error = None + if self._project.is_cloud and self._logged_in: + try: + backtests.extend(list_cloud_backtests(self._project.cloud_id)) + except Exception as exception: + error = exception + self.app.call_from_thread(self._set_backtests, sort_newest_first(backtests), error) + + def _set_backtests(self, backtests: List[BacktestSource], error: Optional[Exception]) -> None: + if error is not None: + self.notify(f"Could not load cloud backtests: {error}", severity="error") + + current = self.backtest + if self._live is not None: + # The backtest being run stays first, the listing may not include it yet + backtests = [self._live.source] + [b for b in backtests if b.id != self._live.source.id] + self._backtests = backtests + + if current is not None and any(b.id == current.id for b in backtests): + # Keep showing the same backtest, and the object already holding its results + self._selected = next(i for i, b in enumerate(backtests) if b.id == current.id) + self._backtests[self._selected] = current + self._update_position() + else: + self._selected = 0 + self._show_backtest() + + def action_pick_backtest(self) -> None: + if not self._backtests: + return + + def picked(index: Optional[int]) -> None: + if index is not None and index != self._selected: + self._selected = index + self._show_backtest() + + self.app.push_screen(BacktestPicker(self._backtests, self._selected), picked) + + def action_step_backtest(self, step: int) -> None: + index = self._selected + step + if 0 <= index < len(self._backtests): + self._selected = index + self._show_backtest() + + def action_show_tab(self, tab: str) -> None: + self.query_one(TabbedContent).active = tab + + def action_reload(self) -> None: + self._orders_loaded_for = None + if not self._is_live(self.backtest): + self._log_loaded_for = None + self._load() + + # The backtest the UI is running + + def set_live_source(self, source: BacktestSource) -> None: + """Replaces the backtest being run, once a new cloud backtest has an id.""" + if self._live is None: + return + was_selected = self._is_live(self.backtest) + old_source = self._live.source + self._live.source = source + self._backtests = [source] + [b for b in self._backtests if b is not old_source and b.id != source.id] + if was_selected: + self._selected = 0 + self._result = None + self._update_position() + self._load() + else: + self._selected = next((i for i, b in enumerate(self._backtests) if b is self.backtest), 0) + + def add_live_line(self, line: str) -> None: + if self._live is None: + return + self._live.lines.append(line) + if self._is_live(self.backtest): + self.query_one("#log", RichLog).write(Text.from_ansi(line)) + + def live_finished(self, exit_code: int) -> None: + if self._live is None: + return + self._live.exit_code = exit_code + if self._is_live(self.backtest): + self._load() + + @property + def live_source(self) -> Optional[BacktestSource]: + return self._live.source if self._live is not None else None + + def _is_live(self, source: Optional[BacktestSource]) -> bool: + return self._live is not None and source is not None and source is self._live.source + + # Loading results + + def _show_backtest(self) -> None: + self._result = None + self._orders_loaded_for = None + self._log_loaded_for = None + self.query_one("#orders-table", DataTable).clear() + self.query_one(TabbedContent).get_tab("orders").label = "Orders" + log = self.query_one("#log", RichLog) + log.clear() + if self._is_live(self.backtest): + for line in self._live.lines: + log.write(Text.from_ansi(line)) + self._log_loaded_for = self.backtest.id + + self._update_position() + self._show_summary() + self._show_chart() + self._show_statistics() + self._load() + + def _update_position(self) -> None: + summary = self.query_one("#summary", Static) + summary.border_subtitle = f"{self._selected + 1}/{len(self._backtests)} [ ] newer/older, o all" \ + if self._backtests else "" + + def _load(self) -> None: + if self._poll_timer is not None: + self._poll_timer.stop() + self._poll_timer = None + backtest = self.backtest + if backtest is None: + self._show_summary() + return + tab = self.query_one(TabbedContent).active + self._load_result(backtest, + load_orders=tab == "orders", + load_log=tab == "logs" and not self._is_live(backtest)) + + @work(thread=True, exclusive=True, group="result") + def _load_result(self, backtest: BacktestSource, load_orders: bool, load_log: bool) -> None: + worker = get_current_worker() + error = None + result = orders = log = None + try: + result = backtest.load() + if load_orders: + orders = backtest.load_orders() + if load_log: + log = backtest.load_log() + except Exception as exception: + error = exception + if not worker.is_cancelled: + self.app.call_from_thread(self._show_result, backtest, result, orders, log, error) + + def _show_result(self, backtest: BacktestSource, result: Optional[BacktestResult], orders: Optional[List[Order]], + log: Optional[List[str]], error: Optional[Exception]) -> None: + if backtest is not self.backtest: + return + if error is not None: + self.notify(f"Could not load the backtest: {error}", severity="error") + if result is not None: + self._result = result + self._apply_live_exit() + self._show_summary() + self._show_chart() + self._show_statistics() + if orders is not None: + self._show_orders(orders) + self._orders_loaded_for = backtest.id + if log is not None: + self._show_log(log) + self._log_loaded_for = backtest.id + + if self._should_poll(): + self._poll_timer = self.set_timer(backtest.poll_seconds, self._poll) + + def _apply_live_exit(self) -> None: + # A local run that was stopped leaves its status at Running, the end of the command ends it + if self._is_live(self.backtest) and self._live.exit_code is not None and self.backtest.location == "local": + self._result.finished = True + + def _should_poll(self) -> bool: + if self._result is not None: + return not self._result.finished + # Without results keep waiting while the backtest is being started + return self._is_live(self.backtest) and self._live.exit_code is None + + def _poll(self) -> None: + self._orders_loaded_for = None + if not self._is_live(self.backtest): + self._log_loaded_for = None + self._load() + + @on(TabbedContent.TabActivated) + def _tab_activated(self, event: TabbedContent.TabActivated) -> None: + backtest = self.backtest + if backtest is None: + return + tab = event.pane.id + if (tab == "orders" and self._orders_loaded_for != backtest.id) or \ + (tab == "logs" and self._log_loaded_for != backtest.id): + self._load() + + # Rendering + + def _status(self) -> str: + result = self._result + live = self._live if self._is_live(self.backtest) else None + if result is None: + if live is not None and live.exit_code not in (None, 0): + return f"failed (exit code {live.exit_code}), see Logs" + if live is not None and live.exit_code is None: + return "starting, see Logs" if self.backtest.location == "local" else "pushing and compiling, see Logs" + return "loading..." + status = result.status or "Running" + if not result.finished and result.progress is not None: + status = f"{status} {result.progress:.0%}" + return status + + def _show_summary(self) -> None: + summary = self.query_one("#summary", Static) + backtest = self.backtest + if backtest is None: + summary.border_title = "No backtests" + summary.update(Text("This project has no backtests yet. Press escape, then b or c to run one.", + style="dim")) + return + + name = self._result.name if self._result is not None and self._result.name else backtest.name + summary.border_title = f"{name} ({backtest.location}): {self._status()}" + + result = self._result + if result is None or not result.runtime_statistics: + summary.update(Text("Waiting for results...", style="dim")) + return + + grid = Table.grid(padding=(0, 3), expand=True) + names = [n for n in SUMMARY_STATISTICS if n in result.runtime_statistics] + for _ in names: + grid.add_column() + grid.add_row(*[Text("PSR" if n == "Probabilistic Sharpe Ratio" else n, style="dim") for n in names]) + grid.add_row(*[_style_statistic(n, result.runtime_statistics[n]) for n in names]) + if result.runtime_error: + grid.add_row(Text(result.runtime_error, style="bold red")) + summary.update(grid) + + def _show_chart(self) -> None: + result = self._result + message = "Waiting for results..." if result is None or not result.finished else "No equity data" + self.query_one("#equity", EquityChart).set_points(result.equity if result is not None else [], message) + + def _show_statistics(self) -> None: + table = self.query_one("#statistics-table", Static) + result = self._result + if result is None or not (result.statistics or result.parameters): + table.update(Text("Statistics are available when the backtest completes.", style="dim")) + return + + grid = Table.grid(padding=(0, 2), expand=True) + for style in ["dim", None, "dim", None]: + grid.add_column(style=style) + items = list(result.statistics.items()) + half = (len(items) + 1) // 2 + for left, right in zip(items[:half], items[half:] + [("", "")]): + grid.add_row(left[0], _style_statistic(*left), right[0], _style_statistic(*right)) + if result.parameters: + grid.add_row("", "", "", "") + grid.add_row(Text("Parameters", style="bold"), "", "", "") + for name, value in result.parameters.items(): + grid.add_row(name, str(value), "", "") + table.update(grid) + + def _show_orders(self, orders: List[Order]) -> None: + table = self.query_one("#orders-table", DataTable) + table.clear() + for order in orders: + table.add_row(str(order.id), + order.time.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") if order.time else "", + order.symbol, + order.type, + Text(order.direction, style="green" if order.direction == "Buy" else "red"), + f"{order.quantity:,.6g}", + f"{order.price:,.2f}", + f"{order.value:,.2f}", + order.status, + order.tag) + self.query_one(TabbedContent).get_tab("orders").label = f"Orders ({len(orders)})" + + def _show_log(self, lines: List[str]) -> None: + log = self.query_one("#log", RichLog) + log.clear() + log.write("\n".join(lines) if lines else Text("No log lines", style="dim")) diff --git a/lean/ui/results.py b/lean/ui/results.py index 0cf98412c..60394ac5f 100644 --- a/lean/ui/results.py +++ b/lean/ui/results.py @@ -1,228 +1,425 @@ -# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. -# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. - -# Reads LEAN backtest results. Local result files and the cloud API share the same JSON format for charts, -# statistics and orders, so both are parsed here. - -from dataclasses import dataclass, field -from datetime import datetime, timezone -from json import loads -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -# LEAN enums, see Common/Orders/OrderTypes.cs and Common/Orders/OrderEnums.cs -ORDER_TYPES = ["Market", "Limit", "Stop Market", "Stop Limit", "Market On Open", "Market On Close", - "Option Exercise", "Limit If Touched", "Combo Market", "Combo Limit", "Combo Leg Limit", - "Trailing Stop"] -ORDER_STATUSES = {0: "New", 1: "Submitted", 2: "Partially Filled", 3: "Filled", 5: "Canceled", 6: "None", - 7: "Invalid", 8: "Cancel Pending", 9: "Update Submitted"} -ORDER_DIRECTIONS = ["Buy", "Sell", "Hold"] - -# The candle series type, whose points are [time, open, high, low, close], see Common/SeriesType.cs -CANDLE_SERIES_TYPE = 2 - -# Files written next to the result file that are not the result itself -_NON_RESULT_SUFFIXES = ("-order-events.json", "-summary.json") - - -@dataclass -class Series: - name: str - unit: str - series_type: int - points: List[Tuple[float, float]] - """(unix time, value) pairs; candle series are reduced to their closing value.""" - - -@dataclass -class Chart: - name: str - series: Dict[str, Series] - - -@dataclass -class Order: - id: int - time: Optional[datetime] - symbol: str - type: str - direction: str - quantity: float - price: float - value: float - status: str - tag: str - - -@dataclass -class BacktestResult: - charts: Dict[str, Chart] = field(default_factory=dict) - statistics: Dict[str, str] = field(default_factory=dict) - runtime_statistics: Dict[str, str] = field(default_factory=dict) - orders: List[Order] = field(default_factory=list) - status: str = "" - runtime_error: str = "" - start: Optional[datetime] = None - end: Optional[datetime] = None - parameters: Dict[str, str] = field(default_factory=dict) - - def series(self, chart: str, series: str) -> Optional[Series]: - found = self.charts.get(chart) - return found.series.get(series) if found is not None else None - - @property - def equity(self) -> Optional[Series]: - return self.series("Strategy Equity", "Equity") - - @property - def drawdown(self) -> Optional[Series]: - return self.series("Drawdown", "Equity Drawdown") - - @property - def benchmark(self) -> Optional[Series]: - return self.series("Benchmark", "Benchmark") - - @property - def progress(self) -> Optional[float]: - """How far the backtest got through its date range, between 0 and 1, based on the last equity point.""" - equity = self.equity - if self.status == "Completed": - return 1.0 - if equity is None or not equity.points or self.start is None or self.end is None: - return None - total = (self.end - self.start).total_seconds() - done = equity.points[-1][0] - self.start.timestamp() - return min(max(done / total, 0.0), 1.0) if total > 0 else None - - -def _parse_time(value: Any) -> Optional[datetime]: - if not value: - return None - if isinstance(value, (int, float)): - return datetime.fromtimestamp(value, tz=timezone.utc) - try: - return datetime.fromisoformat(str(value).replace("Z", "+00:00")) - except ValueError: - return None - - -def _parse_point(value: Any) -> Optional[Tuple[float, float]]: - """Parses a chart point, which is [time, value], [time, open, high, low, close] or {"x": time, "y": value}.""" - if isinstance(value, dict): - time, number = value.get("x"), value.get("y") - elif isinstance(value, list) and len(value) >= 2: - time, number = value[0], value[-1] - else: - return None - if time is None or number is None: - return None - return float(time), float(number) - - -def _parse_series(data: Dict[str, Any]) -> Series: - points = [p for p in (_parse_point(v) for v in data.get("values") or []) if p is not None] - return Series(name=data.get("name", ""), - unit=data.get("unit", ""), - series_type=data.get("seriesType", 0), - points=points) - - -def _parse_order(data: Dict[str, Any]) -> Order: - symbol = data.get("symbol") or {} - order_type = data.get("type", 0) - direction = data.get("direction", 0) - return Order(id=data.get("id", 0), - time=_parse_time(data.get("time")), - symbol=symbol.get("value", "") if isinstance(symbol, dict) else str(symbol), - type=ORDER_TYPES[order_type] if 0 <= order_type < len(ORDER_TYPES) else str(order_type), - direction=ORDER_DIRECTIONS[direction] if 0 <= direction < len(ORDER_DIRECTIONS) else str(direction), - quantity=data.get("quantity", 0.0), - price=data.get("price", 0.0), - value=data.get("value", 0.0), - status=ORDER_STATUSES.get(data.get("status"), str(data.get("status"))), - tag=data.get("tag") or "") - - -def parse_backtest_result(data: Dict[str, Any]) -> BacktestResult: - """Parses LEAN backtest results, as stored in the result file of a local backtest.""" - charts = {} - for name, chart in (data.get("charts") or {}).items(): - charts[name] = Chart(name=name, - series={n: _parse_series(s) for n, s in (chart.get("series") or {}).items()}) - - orders = data.get("orders") or {} - if isinstance(orders, dict): - orders = list(orders.values()) - - state = data.get("state") or {} - configuration = data.get("algorithmConfiguration") or {} - - return BacktestResult(charts=charts, - statistics=data.get("statistics") or {}, - runtime_statistics=data.get("runtimeStatistics") or {}, - orders=sorted((_parse_order(o) for o in orders), key=lambda o: o.id), - status=state.get("Status", ""), - runtime_error=state.get("RuntimeError", ""), - start=_parse_time(configuration.get("startDate")), - end=_parse_time(configuration.get("endDate")), - parameters=configuration.get("parameters") or {}) - - -@dataclass -class LocalBacktest: - """A backtest stored in a project's backtests directory.""" - - path: Path - - @property - def name(self) -> str: - return self.path.name - - @property - def result_file(self) -> Optional[Path]: - """The result file, which LEAN names after the backtest id and rewrites while the backtest runs.""" - config_file = self.path / "config" - if config_file.is_file(): - try: - result_file = self.path / f"{loads(config_file.read_text(encoding='utf-8'))['id']}.json" - if result_file.is_file(): - return result_file - except (ValueError, KeyError): - pass - - candidates = [f for f in self.path.glob("*.json") - if f.stem.lstrip("L-").isdigit() and not f.name.endswith(_NON_RESULT_SUFFIXES)] - return max(candidates, key=lambda f: f.stat().st_mtime) if candidates else None - - @property - def log_file(self) -> Path: - return self.path / "log.txt" - - def load(self) -> Optional[BacktestResult]: - """Reads the result file, returns None if there is none yet or it is being written.""" - result_file = self.result_file - if result_file is None: - return None - try: - return parse_backtest_result(loads(result_file.read_text(encoding="utf-8"))) - except ValueError: - return None - - -def list_local_backtests(project_dir: Path) -> List[LocalBacktest]: - """Returns the backtests of a project, newest first.""" - backtests_dir = project_dir / "backtests" - if not backtests_dir.is_dir(): - return [] - return sorted((LocalBacktest(d) for d in backtests_dir.iterdir() if d.is_dir()), - key=lambda b: b.name, reverse=True) +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. + +# Reads LEAN backtest results from local result files and from the cloud API, which share the same JSON format +# for charts, statistics and orders. Only the Strategy Equity chart is read. + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from json import loads +from re import compile +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +EQUITY_CHART = "Strategy Equity" +EQUITY_SERIES = "Equity" + +# LEAN enums, see Common/Orders/OrderTypes.cs and Common/Orders/OrderEnums.cs +ORDER_TYPES = ["Market", "Limit", "Stop Market", "Stop Limit", "Market On Open", "Market On Close", + "Option Exercise", "Limit If Touched", "Combo Market", "Combo Limit", "Combo Leg Limit", + "Trailing Stop"] +ORDER_STATUSES = {0: "New", 1: "Submitted", 2: "Partially Filled", 3: "Filled", 5: "Canceled", 6: "None", + 7: "Invalid", 8: "Cancel Pending", 9: "Update Submitted"} +ORDER_DIRECTIONS = ["Buy", "Sell", "Hold"] + +# Algorithm statuses after which a backtest's results no longer change, see Common/AlgorithmStatus.cs +FINISHED_STATUSES = {"Completed", "RuntimeError", "Runtime Error", "Stopped", "Liquidated", "Deleted"} + +# The cloud API returns at most this many orders and log lines per request +CLOUD_ORDERS_PAGE = 100 +CLOUD_LOG_PAGE = 200 +MAX_LOG_LINES = 2000 + +# Points to request of the equity chart from the cloud, which samples it down to this many +CLOUD_CHART_POINTS = 1000 + +# LEAN logs the date range when a backtest starts, before the result file has it +_DATA_STREAM_RANGE = compile(r"Begin DataStream - Start: (.+?) Stop: (.+?) Time:") +_LEAN_LOG_TIME_FORMAT = "%m/%d/%Y %I:%M:%S %p" + +# Files written next to a local result file that are not the result itself +_NON_RESULT_SUFFIXES = ("-order-events.json", "-summary.json") + + +@dataclass +class Order: + id: int + time: Optional[datetime] + symbol: str + type: str + direction: str + quantity: float + price: float + value: float + status: str + tag: str + + +@dataclass +class BacktestResult: + name: str = "" + equity: List[Tuple[float, float]] = field(default_factory=list) + """(unix time, equity) points of the Strategy Equity chart, candles reduced to their closing value.""" + statistics: Dict[str, str] = field(default_factory=dict) + runtime_statistics: Dict[str, str] = field(default_factory=dict) + orders: Optional[List[Order]] = None + """The orders, or None if they are loaded separately (see BacktestSource.load_orders).""" + status: str = "" + finished: bool = False + runtime_error: str = "" + start: Optional[datetime] = None + end: Optional[datetime] = None + parameters: Dict[str, str] = field(default_factory=dict) + reported_progress: Optional[float] = None + + @property + def progress(self) -> Optional[float]: + """How far the backtest is, between 0 and 1. + + Cloud backtests report it, for local backtests it is how far the equity chart got through the date range. + """ + if self.finished: + return 1.0 + if self.reported_progress is not None: + return self.reported_progress + if not self.equity or self.start is None or self.end is None: + return None + total = (self.end - self.start).total_seconds() + done = self.equity[-1][0] - self.start.timestamp() + return min(max(done / total, 0.0), 1.0) if total > 0 else None + + +def _parse_time(value: Any) -> Optional[datetime]: + """Parses a LEAN time, either unix seconds or an ISO 8601 string, which is UTC unless it says otherwise.""" + if not value: + return None + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value, tz=timezone.utc) + try: + time = datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError: + return None + return time if time.tzinfo is not None else time.replace(tzinfo=timezone.utc) + + +def _parse_point(value: Any) -> Optional[Tuple[float, float]]: + """Parses a chart point, which is [time, value], [time, open, high, low, close] or {"x": time, "y": value}.""" + if isinstance(value, dict): + time, number = value.get("x"), value.get("y") + elif isinstance(value, list) and len(value) >= 2: + time, number = value[0], value[-1] + else: + return None + if time is None or number is None: + return None + return float(time), float(number) + + +def parse_equity(charts: Dict[str, Any]) -> List[Tuple[float, float]]: + """Returns the points of the Equity series of the Strategy Equity chart.""" + series = ((charts.get(EQUITY_CHART) or {}).get("series") or {}).get(EQUITY_SERIES) or {} + return [p for p in (_parse_point(v) for v in series.get("values") or []) if p is not None] + + +def parse_order(data: Dict[str, Any]) -> Order: + symbol = data.get("symbol") or {} + order_type = data.get("type", 0) + direction = data.get("direction", 0) + return Order(id=data.get("id", 0), + time=_parse_time(data.get("time")), + symbol=symbol.get("value", "") if isinstance(symbol, dict) else str(symbol), + type=ORDER_TYPES[order_type] if 0 <= order_type < len(ORDER_TYPES) else str(order_type), + direction=ORDER_DIRECTIONS[direction] if 0 <= direction < len(ORDER_DIRECTIONS) else str(direction), + quantity=data.get("quantity", 0.0), + price=data.get("price", 0.0), + value=data.get("value", 0.0), + status=ORDER_STATUSES.get(data.get("status"), str(data.get("status"))), + tag=data.get("tag") or "") + + +def parse_orders(orders: Any) -> List[Order]: + """Parses orders, which local results store by id and the cloud API returns as a list.""" + if isinstance(orders, dict): + orders = list(orders.values()) + return sorted((parse_order(o) for o in orders or []), key=lambda o: o.id) + + +def parse_local_result(data: Dict[str, Any]) -> BacktestResult: + """Parses the result file LEAN writes while running a backtest locally.""" + state = data.get("state") or {} + configuration = data.get("algorithmConfiguration") or {} + status = state.get("Status", "") + + return BacktestResult(equity=parse_equity(data.get("charts") or {}), + statistics=data.get("statistics") or {}, + runtime_statistics=data.get("runtimeStatistics") or {}, + orders=parse_orders(data.get("orders")), + status=status or "Running", + finished=status in FINISHED_STATUSES, + runtime_error=state.get("RuntimeError", ""), + start=_parse_time(configuration.get("startDate")), + end=_parse_time(configuration.get("endDate")), + parameters=configuration.get("parameters") or {}) + + +def parse_cloud_backtest(backtest: Dict[str, Any], charts: Dict[str, Any]) -> BacktestResult: + """Parses a backtest from the backtests/read endpoint and its equity chart from backtests/chart/read.""" + statistics = backtest.get("statistics") + parameters = backtest.get("parameterSet") + return BacktestResult(name=backtest.get("name", ""), + equity=parse_equity(charts), + statistics=statistics if isinstance(statistics, dict) else {}, + runtime_statistics=backtest.get("runtimeStatistics") or {}, + status=(backtest.get("status") or "").rstrip("."), + finished=bool(backtest.get("completed")), + runtime_error=backtest.get("error") or "", + start=_parse_time(backtest.get("backtestStart")), + end=_parse_time(backtest.get("backtestEnd")), + parameters=parameters if isinstance(parameters, dict) else {}, + reported_progress=backtest.get("progress")) + + +class BacktestSource: + """A backtest whose results can be shown, stored locally or in the cloud.""" + + location = "" + poll_seconds = 15 + + @property + def id(self) -> str: + raise NotImplementedError() + + @property + def name(self) -> str: + raise NotImplementedError() + + @property + def created(self) -> Optional[datetime]: + raise NotImplementedError() + + def load(self) -> Optional[BacktestResult]: + """Loads the summary and equity chart, returns None if there are no results yet. Blocks.""" + raise NotImplementedError() + + def load_orders(self) -> List[Order]: + raise NotImplementedError() + + def load_log(self) -> List[str]: + raise NotImplementedError() + + +class LocalBacktest(BacktestSource): + """A backtest stored in a project's backtests directory, whose result file LEAN rewrites while it runs.""" + + location = "local" + poll_seconds = 10 + + def __init__(self, path: Path) -> None: + self.path = path + self._last_result: Optional[BacktestResult] = None + + @property + def id(self) -> str: + return str(self.path) + + @property + def name(self) -> str: + return self.path.name + + @property + def created(self) -> Optional[datetime]: + try: + return datetime.strptime(self.path.name, "%Y-%m-%d_%H-%M-%S").astimezone(timezone.utc) + except ValueError: + return None + + @property + def result_file(self) -> Optional[Path]: + """The result file, which LEAN names after the backtest id.""" + config_file = self.path / "config" + if config_file.is_file(): + try: + result_file = self.path / f"{loads(config_file.read_text(encoding='utf-8'))['id']}.json" + if result_file.is_file(): + return result_file + except (ValueError, KeyError): + pass + + candidates = [f for f in self.path.glob("*.json") + if f.stem.lstrip("L-").isdigit() and not f.name.endswith(_NON_RESULT_SUFFIXES)] + return max(candidates, key=lambda f: f.stat().st_mtime) if candidates else None + + def load(self) -> Optional[BacktestResult]: + """Reads the result file, returns None if there is none yet or it is being written.""" + result_file = self.result_file + if result_file is None: + return None + try: + result = parse_local_result(loads(result_file.read_text(encoding="utf-8"))) + except ValueError: + return None + result.name = self.name + if result.start is None or result.end is None: + result.start, result.end = self._date_range_from_log() + self._last_result = result + return result + + def _date_range_from_log(self) -> Tuple[Optional[datetime], Optional[datetime]]: + """Reads the backtest's date range from its log, which has it from the start of the run.""" + log_file = self.path / "log.txt" + if not log_file.is_file(): + return None, None + with log_file.open(encoding="utf-8", errors="replace") as file: + for line in file: + match = _DATA_STREAM_RANGE.search(line) + if match is not None: + try: + return tuple(datetime.strptime(m, _LEAN_LOG_TIME_FORMAT).replace(tzinfo=timezone.utc) + for m in match.groups()) + except ValueError: + break + return None, None + + def load_orders(self) -> List[Order]: + result = self._last_result or self.load() + return result.orders or [] if result is not None else [] + + def load_log(self) -> List[str]: + log_file = self.path / "log.txt" + if not log_file.is_file(): + return [] + return log_file.read_text(encoding="utf-8", errors="replace").splitlines()[-MAX_LOG_LINES:] + + +class CloudBacktest(BacktestSource): + """A backtest in the cloud, read through the API.""" + + location = "cloud" + poll_seconds = 15 + + def __init__(self, project_id: int, backtest_id: str, name: str = "", created: Optional[datetime] = None) -> None: + self.project_id = project_id + self.backtest_id = backtest_id + self._name = name + self._created = created + + @property + def id(self) -> str: + return self.backtest_id + + @property + def name(self) -> str: + return self._name or self.backtest_id + + @property + def created(self) -> Optional[datetime]: + return self._created + + def _post(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]: + from lean.container import container + return container.api_client.post(endpoint, {"projectId": self.project_id, "backtestId": self.backtest_id, + **payload}) + + def load(self) -> Optional[BacktestResult]: + backtest = self._post("backtests/read", {})["backtest"] + self._name = backtest.get("name") or self._name + self._created = _parse_time(backtest.get("created")) or self._created + + charts = {} + start, end = _parse_time(backtest.get("backtestStart")), _parse_time(backtest.get("backtestEnd")) + if EQUITY_CHART in (backtest.get("charts") or {}) and start is not None and end is not None: + try: + chart = self._post("backtests/chart/read", {"name": EQUITY_CHART, "count": CLOUD_CHART_POINTS, + "start": int(start.timestamp()), + "end": int(end.timestamp())}).get("chart") + if chart: + charts[EQUITY_CHART] = chart + except Exception: + # The chart is not available until the backtest has produced some results + pass + + return parse_cloud_backtest(backtest, charts) + + def load_orders(self) -> List[Order]: + orders: List[Dict[str, Any]] = [] + while True: + page = self._post("backtests/orders/read", {"start": len(orders), "end": len(orders) + CLOUD_ORDERS_PAGE}) + orders.extend(page.get("orders") or []) + if not page.get("orders") or len(orders) >= page.get("length", 0): + return parse_orders(orders) + + def load_log(self) -> List[str]: + first = self._post("backtests/read/log", {"start": 0, "end": CLOUD_LOG_PAGE, "query": " "}) + length = first.get("length", 0) + start = max(0, length - MAX_LOG_LINES) + lines: List[str] = list(first.get("logs") or []) if start == 0 else [] + position = len(lines) if start == 0 else start + while position < length: + page = self._post("backtests/read/log", {"start": position, "end": min(position + CLOUD_LOG_PAGE, length), + "query": " "}) + if not page.get("logs"): + break + lines.extend(page["logs"]) + position += len(page["logs"]) + return lines + + +class PendingCloudBacktest(BacktestSource): + """A cloud backtest being pushed and compiled, which has no id or results yet.""" + + location = "cloud" + + @property + def id(self) -> str: + return "pending" + + @property + def name(self) -> str: + return "New cloud backtest" + + @property + def created(self) -> Optional[datetime]: + return datetime.now(timezone.utc) + + def load(self) -> Optional[BacktestResult]: + return None + + def load_orders(self) -> List[Order]: + return [] + + def load_log(self) -> List[str]: + return [] + + +def list_local_backtests(project_dir: Optional[Path]) -> List[LocalBacktest]: + """Returns the backtests of a local project, newest first.""" + backtests_dir = project_dir / "backtests" if project_dir is not None else None + if backtests_dir is None or not backtests_dir.is_dir(): + return [] + return sorted((LocalBacktest(d) for d in backtests_dir.iterdir() if d.is_dir()), + key=lambda b: b.name, reverse=True) + + +def list_cloud_backtests(project_id: int) -> List[CloudBacktest]: + """Returns the backtests of a cloud project, newest first. Makes a blocking API request.""" + from lean.container import container + + data = container.api_client.post("backtests/list", {"projectId": project_id, "includeStatistics": False}) + backtests = [CloudBacktest(project_id, b["backtestId"], b.get("name", ""), _parse_time(b.get("created"))) + for b in data.get("backtests") or []] + return sorted(backtests, key=lambda b: b.created or datetime.min.replace(tzinfo=timezone.utc), reverse=True) + + +def sort_newest_first(backtests: List[BacktestSource]) -> List[BacktestSource]: + oldest = datetime.min.replace(tzinfo=timezone.utc) + return sorted(backtests, key=lambda b: b.created or oldest, reverse=True) diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index 7bd680cba..220d7b50c 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -20,14 +20,15 @@ from textual.widgets import TabbedContent from lean.commands import lean -from lean.container import container from lean.ui import app as ui_app -from lean.ui.actions import ACTIONS, format_command +from lean.ui.actions import ACTIONS, CLOUD_BACKTEST_URL, format_command from lean.ui.app import ConfirmScreen, LeanApp, ProjectTable -from lean.ui.backtest_screen import BacktestScreen +from lean.ui.backtest_screen import BacktestPicker, BacktestScreen from lean.ui.projects import UIProject, discover_local_projects, filter_projects, merge_cloud_projects from tests.test_helpers import create_api_project, create_fake_lean_cli_directory -from tests.test_ui_results import create_local_backtest, create_result +from lean.ui.results import CloudBacktest +from tests.test_ui_results import (BACKTEST_ID, create_cloud_backtest, create_local_backtest, create_result, + mock_cloud_api) def _discover() -> list: @@ -102,11 +103,25 @@ def test_actions_are_unavailable_when_project_or_login_is_missing() -> None: assert _action("b").unavailable_reason(local, logged_in=False) is None assert _action("b").unavailable_reason(cloud, logged_in=True) == "pull it first" + assert _action("c").unavailable_reason(local, logged_in=False) == "run `lean login` first" + assert _action("c").unavailable_reason(cloud, logged_in=True) is None assert _action("p").unavailable_reason(local, logged_in=False) == "run `lean login` first" assert _action("u").unavailable_reason(local, logged_in=True) == "push it first" assert _action("u").unavailable_reason(cloud, logged_in=True) is None +def test_view_results_needs_local_backtests_or_a_cloud_project() -> None: + local = UIProject("Local", Path.cwd() / "Local") + cloud = UIProject("Cloud", cloud_id=5) + + assert _action("v").unavailable_reason(local, logged_in=True) == "no backtests yet" + assert _action("v").unavailable_reason(cloud, logged_in=False) == "run `lean login` first" + assert _action("v").unavailable_reason(cloud, logged_in=True) is None + + create_local_backtest(local.path, "2026-10-08_16-00-57", create_result()) + assert _action("v").unavailable_reason(local, logged_in=False) is None + + def test_action_commands() -> None: local = UIProject("My Project", Path("My Project")) cloud = UIProject("Cloud Project", cloud_id=5) @@ -117,6 +132,12 @@ def test_action_commands() -> None: assert _action("u").build_args(cloud) == ["cloud", "pull", "--project", "5"] +def test_cloud_backtest_url_holds_the_project_and_backtest_ids() -> None: + match = CLOUD_BACKTEST_URL.search(f"Backtest url: https://www.quantconnect.com/project/19213997/{BACKTEST_ID}") + + assert match.groups() == ("19213997", BACKTEST_ID) + + def test_ui_requires_lean_config() -> None: result = CliRunner().invoke(lean, ["ui"]) @@ -124,21 +145,42 @@ def test_ui_requires_lean_config() -> None: assert "requires a Lean configuration file" in str(result.exception) -def _run_app(test) -> None: - """Runs a test coroutine against the app with the fake Lean CLI directory and no stored credentials.""" +def test_command_process_runs_the_cli_as_a_child_process(fake_filesystem) -> None: + from lean import __version__ + from lean.ui.actions import CommandProcess + + lines = [] + fake_filesystem.pause() + try: + exit_code = CommandProcess(["--version"], Path(__file__).parent).run(lines.append) + finally: + fake_filesystem.resume() + + assert exit_code == 0 + assert lines == [f"lean {__version__}"] + + +def _run_app(test, logged_in: bool = False) -> None: + """Runs a test coroutine against the app with the fake Lean CLI directory and no cloud projects.""" create_fake_lean_cli_directory() - container.cli_config_manager.user_id.get_value = mock.Mock(return_value=None) async def run() -> None: - app = LeanApp(Path.cwd(), Path.cwd() / "data") - async with app.run_test(size=(140, 40)) as pilot: - await app.workers.wait_for_complete() - await pilot.pause() - await test(app, pilot) + with mock.patch.object(ui_app, "is_logged_in", return_value=logged_in), \ + mock.patch.object(ui_app, "fetch_cloud_projects", return_value=[]): + app = LeanApp(Path.cwd(), Path.cwd() / "data") + async with app.run_test(size=(140, 40)) as pilot: + await _settle(app, pilot) + await test(app, pilot) asyncio.run(run()) +async def _settle(app: LeanApp, pilot) -> None: + for _ in range(3): + await app.workers.wait_for_complete() + await pilot.pause() + + def test_app_lists_local_projects_and_filters_them() -> None: async def test(app: LeanApp, pilot) -> None: table = app.query_one(ProjectTable) @@ -168,42 +210,37 @@ async def test(app: LeanApp, pilot) -> None: _run_app(test) -def test_app_asks_for_confirmation_before_running_a_command() -> None: +def test_app_asks_for_confirmation_before_pushing() -> None: async def test(app: LeanApp, pilot) -> None: with mock.patch.object(ui_app, "CommandProcess") as command_process: - await pilot.press("b") + await pilot.press("p") assert isinstance(app.screen, ConfirmScreen) - assert app.screen._command == "lean backtest 'CSharp Project'" + assert app.screen._command == "lean cloud push --project 'CSharp Project'" await pilot.press("n") assert not isinstance(app.screen, ConfirmScreen) command_process.assert_not_called() - _run_app(test) + _run_app(test, logged_in=True) -def test_app_runs_the_selected_action_and_shows_its_output() -> None: +def test_app_runs_a_confirmed_command_and_shows_its_output() -> None: async def test(app: LeanApp, pilot) -> None: def fake_run(on_line): - on_line("Backtest finished") + on_line("Successfully pushed 'CSharp Project'") return 0 with mock.patch.object(ui_app, "CommandProcess") as command_process: command_process.return_value.run.side_effect = fake_run + await pilot.press("p", "y") + await _settle(app, pilot) - await pilot.press("enter") - assert app.focused.__class__.__name__ == "ActionList" - await pilot.press("enter", "y") - await app.workers.wait_for_complete() - await pilot.pause() - - command_process.assert_called_once_with(["backtest", "CSharp Project"], Path.cwd()) + command_process.assert_called_once_with(["cloud", "push", "--project", "CSharp Project"], Path.cwd()) output = app.query_one("#output") - lines = [line.text for line in output.lines] - assert "Backtest finished" in lines + assert "Successfully pushed 'CSharp Project'" in [line.text for line in output.lines] assert output.border_subtitle == "done" - _run_app(test) + _run_app(test, logged_in=True) def test_app_does_not_run_unavailable_actions() -> None: @@ -216,48 +253,97 @@ async def test(app: LeanApp, pilot) -> None: _run_app(test) -def test_command_process_runs_the_cli_as_a_child_process(fake_filesystem) -> None: - from lean import __version__ - from lean.ui.actions import CommandProcess +def test_backtesting_locally_opens_the_results_while_the_backtest_runs() -> None: + async def test(app: LeanApp, pilot) -> None: + def fake_run(on_line): + output_dir = Path(command_process.call_args.args[0][3]) + on_line("Starting LEAN") + create_local_backtest(output_dir.parent.parent, output_dir.name, create_result()) + return 0 - lines = [] - fake_filesystem.pause() - try: - exit_code = CommandProcess(["--version"], Path(__file__).parent).run(lines.append) - finally: - fake_filesystem.resume() + with mock.patch.object(ui_app, "CommandProcess") as command_process: + command_process.return_value.run.side_effect = fake_run + await pilot.press("b") + assert isinstance(app.screen, BacktestScreen) + await _settle(app, pilot) - assert exit_code == 0 - assert lines == [f"lean {__version__}"] + args = command_process.call_args.args[0] + assert args[:3] == ["backtest", "CSharp Project", "--output"] + assert Path(args[3]).parent == Path.cwd() / "CSharp Project" / "backtests" + + screen = app.screen + assert screen.query_one("#summary").border_title.endswith("(local): Completed") + assert len(screen.query_one("#equity").points) == 5 + assert screen.query_one(TabbedContent).active == "logs" + assert "Starting LEAN" in [line.text for line in screen.query_one("#log").lines] + + _run_app(test) -def test_view_results_is_unavailable_without_local_backtests() -> None: +def test_backtesting_in_the_cloud_switches_to_the_backtest_once_it_is_created() -> None: async def test(app: LeanApp, pilot) -> None: - await pilot.press("v") - assert not isinstance(app.screen, BacktestScreen) - assert _action("v").unavailable_reason(app.selected_project, logged_in=False) == "no local backtests yet" + mock_cloud_api(create_cloud_backtest()) - _run_app(test) + def fake_run(on_line): + on_line("Started compiling project 'CSharp Project'") + on_line(f"Backtest url: https://www.quantconnect.com/project/123/{BACKTEST_ID}") + return 0 + with mock.patch.object(ui_app, "CommandProcess") as command_process: + command_process.return_value.run.side_effect = fake_run + await pilot.press("c") + assert isinstance(app.screen, BacktestScreen) + await _settle(app, pilot) -def test_app_opens_backtest_results_with_charts_statistics_and_orders() -> None: + command_process.assert_called_once_with(["cloud", "backtest", "CSharp Project", "--push"], Path.cwd()) + screen = app.screen + assert isinstance(screen.live_source, CloudBacktest) + assert (screen.live_source.project_id, screen.live_source.backtest_id) == (123, BACKTEST_ID) + assert screen.query_one("#summary").border_title == "Logical Red Monkey (cloud): Completed" + assert len(screen.query_one("#equity").points) == 5 + + _run_app(test, logged_in=True) + + +def test_a_failed_cloud_backtest_shows_the_failure() -> None: async def test(app: LeanApp, pilot) -> None: - create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", create_result()) + def fake_run(on_line): + on_line("Build Error: main.py line 3") + return 1 + + with mock.patch.object(ui_app, "CommandProcess") as command_process: + command_process.return_value.run.side_effect = fake_run + await pilot.press("c") + await _settle(app, pilot) + + screen = app.screen + assert screen.query_one("#summary").border_title == \ + "New cloud backtest (cloud): failed (exit code 1), see Logs" + assert "Build Error: main.py line 3" in [line.text for line in screen.query_one("#log").lines] + + _run_app(test, logged_in=True) + + +def test_viewing_results_shows_the_newest_backtest() -> None: + async def test(app: LeanApp, pilot) -> None: + project_dir = Path.cwd() / "CSharp Project" + create_local_backtest(project_dir, "2026-10-08_16-00-57", create_result()) + create_local_backtest(project_dir, "2026-10-09_09-30-00", create_result()) await pilot.press("v") - await app.workers.wait_for_complete() - await pilot.pause() + await _settle(app, pilot) screen = app.screen assert isinstance(screen, BacktestScreen) - assert screen.query_one("#summary").border_title == "2026-10-08_16-00-57: Completed" - assert screen.query_one("#equity")._lines[0].label == "Equity" - assert screen.query_one("#equity")._lines[1].label == "Benchmark" + assert screen.query_one("#summary").border_title == "2026-10-09_09-30-00 (local): Completed" + + await pilot.press("2") + await _settle(app, pilot) assert screen.query_one("#orders-table").row_count == 2 - await pilot.press("3") - assert screen.query_one(TabbedContent).active == "charts" - assert screen.query_one("#chart-names").option_count == 4 + await pilot.press("right_square_bracket") + await _settle(app, pilot) + assert screen.query_one("#summary").border_title == "2026-10-08_16-00-57 (local): Completed" await pilot.press("escape") assert not isinstance(app.screen, BacktestScreen) @@ -265,34 +351,60 @@ async def test(app: LeanApp, pilot) -> None: _run_app(test) -def test_backtest_screen_switches_between_backtests() -> None: +def test_results_list_local_and_cloud_backtests_newest_first() -> None: + async def test(app: LeanApp, pilot) -> None: + project = UIProject("Cloud Project", Path.cwd() / "Cloud Project", cloud_id=123) + create_local_backtest(project.path, "2026-05-01_12-00-00", create_result()) + mock_cloud_api(create_cloud_backtest()) + + app.push_screen(BacktestScreen(project, logged_in=True)) + await _settle(app, pilot) + + screen = app.screen + assert [(b.location, b.name) for b in screen._backtests] == \ + [("cloud", "Logical Red Monkey"), ("local", "2026-05-01_12-00-00"), ("cloud", "Older")] + assert screen.query_one("#summary").border_title == "Logical Red Monkey (cloud): Completed" + + _run_app(test, logged_in=True) + + +def test_backtest_picker_filters_and_opens_a_backtest() -> None: async def test(app: LeanApp, pilot) -> None: project_dir = Path.cwd() / "CSharp Project" create_local_backtest(project_dir, "2026-10-08_16-00-57", create_result()) - create_local_backtest(project_dir, "2026-10-09_09-30-00", create_result(status="Running")) + create_local_backtest(project_dir, "2026-10-09_09-30-00", create_result()) await pilot.press("v") - await app.workers.wait_for_complete() - await pilot.pause() - assert app.screen.query_one("#summary").border_title.startswith("2026-10-09_09-30-00: Running") + await _settle(app, pilot) + await pilot.press("o") + assert isinstance(app.screen, BacktestPicker) - await pilot.press("j") - await app.workers.wait_for_complete() - await pilot.pause() - assert app.screen.query_one("#summary").border_title == "2026-10-08_16-00-57: Completed" + await pilot.press(*"10-08", "enter") + await _settle(app, pilot) + + assert isinstance(app.screen, BacktestScreen) + assert app.screen.query_one("#summary").border_title == "2026-10-08_16-00-57 (local): Completed" _run_app(test) -def test_backtest_screen_shows_a_message_for_backtests_without_results() -> None: +def test_results_of_a_running_backtest_refresh_until_it_finishes() -> None: async def test(app: LeanApp, pilot) -> None: - (Path.cwd() / "CSharp Project" / "backtests" / "2026-10-08_16-00-57").mkdir(parents=True) + output_dir = create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", + create_result(status="Running", equity_points=3)) await pilot.press("v") - await app.workers.wait_for_complete() - await pilot.pause() + await _settle(app, pilot) + screen = app.screen + assert screen.query_one("#summary").border_title.startswith("2026-10-08_16-00-57 (local): Running ") + assert screen._poll_timer is not None - assert "No results found" in str(app.screen.query_one("#summary").render()) + (output_dir / "1121419604.json").write_text(json.dumps(create_result())) + screen._poll() + await _settle(app, pilot) + + assert screen.query_one("#summary").border_title == "2026-10-08_16-00-57 (local): Completed" + assert screen._poll_timer is None _run_app(test) @@ -301,7 +413,7 @@ def test_project_keys_do_not_run_actions_on_other_screens() -> None: async def test(app: LeanApp, pilot) -> None: create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", create_result()) await pilot.press("v") - await pilot.pause() + await _settle(app, pilot) with mock.patch.object(ui_app, "CommandProcess") as command_process: await pilot.press("b") @@ -313,12 +425,12 @@ async def test(app: LeanApp, pilot) -> None: def test_projects_can_update_while_another_screen_is_shown() -> None: async def test(app: LeanApp, pilot) -> None: - await pilot.press("b") + await pilot.press("p") assert isinstance(app.screen, ConfirmScreen) - app._set_projects(app._projects[:1], logged_in=False, status="updated") + app._set_projects(app._projects[:1], logged_in=True, status="updated") await pilot.pause() assert app.screen_stack[0].query_one(ProjectTable).row_count == 1 - _run_app(test) + _run_app(test, logged_in=True) diff --git a/tests/test_ui_results.py b/tests/test_ui_results.py index d9e4344ee..61dac1b7a 100644 --- a/tests/test_ui_results.py +++ b/tests/test_ui_results.py @@ -14,86 +14,129 @@ import json from datetime import datetime, timezone from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List, Optional +from unittest import mock -from lean.ui.results import LocalBacktest, list_local_backtests, parse_backtest_result +from lean.container import container +from lean.ui.results import (CloudBacktest, LocalBacktest, list_cloud_backtests, list_local_backtests, + parse_cloud_backtest, parse_local_result, sort_newest_first) # 2013-10-07 04:00 UTC to 2013-10-11 20:00 UTC, like the sample data backtests START = 1381118400 END = 1381521600 +BACKTEST_ID = "c9c84362d35b5ab2e8bd457446b74d58" -def create_result(status: str = "Completed", equity_points: int = 5) -> Dict[str, Any]: - """Returns backtest results in the format LEAN writes them, trimmed to a few points.""" - step = (END - START) / max(1, equity_points - 1) - equity = [[START + i * step, 100000 + i, 100000 + i + 5, 100000 + i - 5, 100000 + i * 10] - for i in range(equity_points)] +def create_equity_chart(points: int = 5) -> Dict[str, Any]: + """Returns a Strategy Equity chart, whose equity series holds candles.""" + step = (END - START) / max(1, points - 1) + return {"name": "Strategy Equity", "chartType": 0, "series": { + "Equity": {"name": "Equity", "unit": "$", "index": 0, "seriesType": 2, + "values": [[START + i * step, 100000 + i, 100000 + i + 5, 100000 + i - 5, 100000 + i * 10] + for i in range(points)]}, + "Return": {"name": "Return", "unit": "%", "index": 1, "seriesType": 3, "values": [[START, 0.0]]}, + }} + + +def create_orders() -> Dict[str, Any]: return { - "charts": { - "Strategy Equity": {"name": "Strategy Equity", "chartType": 0, "series": { - "Equity": {"name": "Equity", "unit": "$", "index": 0, "seriesType": 2, "values": equity}, - "Return": {"name": "Return", "unit": "%", "index": 1, "seriesType": 3, - "values": [[START, 0.0], [END, 0.4]]}, - }}, - "Drawdown": {"name": "Drawdown", "chartType": 0, "series": { - "Equity Drawdown": {"name": "Equity Drawdown", "unit": "%", "seriesType": 0, - "values": [[START, 0.0], [END, -0.34]]}, - }}, - "Benchmark": {"name": "Benchmark", "chartType": 0, "series": { - "Benchmark": {"name": "Benchmark", "unit": "$", "seriesType": 0, - "values": [[START, 146.0], [END, 147.0]]}, - }}, - "Indicators": {"name": "Indicators", "chartType": 0, "series": { - "Fast EMA": {"name": "Fast EMA", "unit": "$", "seriesType": 0, - "values": [[START, 145.0], [START + 60, None], {"x": END, "y": 146.0}]}, - }}, - }, - "orders": { - "2": {"type": 0, "id": 2, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, - "price": 145.34, "time": "2013-10-07T14:46:00Z", "quantity": -686.0, "status": 3, - "tag": "Liquidated", "direction": 1, "value": -99705.8}, - "1": {"type": 0, "id": 1, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, - "price": 145.33, "time": "2013-10-07T14:10:00Z", "quantity": 686.0, "status": 3, - "tag": "", "direction": 0, "value": 99693.94}, - }, + "2": {"type": 0, "id": 2, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, + "price": 145.34, "time": "2013-10-07T14:46:00Z", "quantity": -686.0, "status": 3, + "tag": "Liquidated", "direction": 1, "value": -99705.8}, + "1": {"type": 0, "id": 1, "symbol": {"value": "SPY", "id": "SPY R735QTJ8XC9X", "permtick": "SPY"}, + "price": 145.33, "time": "2013-10-07T14:10:00Z", "quantity": 686.0, "status": 3, + "tag": "", "direction": 0, "value": 99693.94}, + } + + +def create_result(status: str = "Completed", equity_points: int = 5) -> Dict[str, Any]: + """Returns local backtest results in the format LEAN writes them, trimmed to a few points. + + LEAN only adds the algorithm configuration once the backtest has finished. + """ + result = { + "charts": {"Strategy Equity": create_equity_chart(equity_points)}, + "orders": create_orders(), "statistics": {"Total Orders": "2", "Net Profit": "-0.160%", "Sharpe Ratio": "1.028"}, "runtimeStatistics": {"Equity": "$99,839.57", "Net Profit": "$-294.86", "Return": "-0.16 %"}, "state": {"Status": status, "RuntimeError": ""}, - "algorithmConfiguration": {"startDate": "2013-10-07T00:00:00Z", "endDate": "2013-10-11T23:59:59Z", - "parameters": {"delay-ms": "30"}}, } + if status != "Running": + result["algorithmConfiguration"] = {"startDate": "2013-10-07T00:00:00Z", "endDate": "2013-10-11T23:59:59Z", + "parameters": {"delay-ms": "30"}} + return result -def create_local_backtest(project_dir: Path, name: str, result: Dict[str, Any], backtest_id: int = 1121419604) -> Path: +def create_local_backtest(project_dir: Path, name: str, result: Optional[Dict[str, Any]], + backtest_id: int = 1121419604) -> Path: """Writes a backtest output directory the way `lean backtest` leaves it.""" output_dir = project_dir / "backtests" / name - output_dir.mkdir(parents=True) + output_dir.mkdir(parents=True, exist_ok=True) (output_dir / "config").write_text(json.dumps({"id": backtest_id, "container": "lean_cli_abc"})) - (output_dir / f"{backtest_id}.json").write_text(json.dumps(result)) + if result is not None: + (output_dir / f"{backtest_id}.json").write_text(json.dumps(result)) (output_dir / f"{backtest_id}-order-events.json").write_text("[]") - (output_dir / "log.txt").write_text("2026-10-08T20:01:00Z TRACE:: Engine.Run(): start\n") + (output_dir / "log.txt").write_text( + "2026-10-08T20:01:00Z TRACE:: Engine.Run(): start\n" + "2026-10-08T20:01:01Z TRACE:: AlgorithmManager.Run(): Begin DataStream - Start: 10/7/2013 12:00:00 AM " + "Stop: 10/11/2013 11:59:59 PM Time: 10/4/2013 3:30:00 PM Warmup: True\n") return output_dir -def test_parse_backtest_result_reads_charts_and_reduces_candles_to_closes() -> None: - result = parse_backtest_result(create_result()) +def create_cloud_backtest(status: str = "Completed.", completed: bool = True, progress: float = 1.0) -> Dict[str, Any]: + """Returns a backtest as the backtests/read endpoint returns it, without chart data.""" + return {"backtestId": BACKTEST_ID, "name": "Logical Red Monkey", "status": status, "completed": completed, + "progress": progress, "created": "2026-10-08 19:45:10", "error": None, + "backtestStart": "2013-10-07 00:00:00", "backtestEnd": "2013-10-11 23:59:59", + "charts": {"Strategy Equity": {"name": "Strategy Equity", "series": {}}}, + "statistics": {"Net Profit": "136.589%"}, "runtimeStatistics": {"Equity": "$236,588.97"}, + "parameterSet": {"roc_window": "150"}} - assert result.equity.unit == "$" - assert result.equity.points[0] == (START, 100000) - assert result.equity.points[-1] == (END, 100040) - assert result.drawdown.points == [(START, 0.0), (END, -0.34)] - assert result.benchmark.points[0] == (START, 146.0) +def mock_cloud_api(backtest: Dict[str, Any], orders: int = 0, logs: int = 0) -> mock.Mock: + """Makes the API client answer the backtest endpoints, paging orders and logs like the API.""" + order_list = [dict(create_orders()["1"], id=i + 1) for i in range(orders)] + log_lines = [f"line {i}" for i in range(logs)] -def test_parse_backtest_result_skips_empty_points_and_reads_xy_points() -> None: - result = parse_backtest_result(create_result()) + def post(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]: + if endpoint == "backtests/read": + return {"backtest": backtest} + if endpoint == "backtests/chart/read": + assert payload["name"] == "Strategy Equity" + return {"chart": create_equity_chart()} + if endpoint == "backtests/orders/read": + assert payload["end"] - payload["start"] <= 100 + return {"orders": order_list[payload["start"]:payload["end"]], "length": len(order_list)} + if endpoint == "backtests/read/log": + assert payload["end"] - payload["start"] <= 200 + return {"logs": log_lines[payload["start"]:payload["end"]], "length": len(log_lines)} + if endpoint == "backtests/list": + return {"backtests": [{"backtestId": "a" * 32, "name": "Older", "created": "2026-01-01 10:00:00"}, + {"backtestId": "b" * 32, "name": "Newer", "created": "2026-10-08 19:45:10"}]} + raise AssertionError(f"Unexpected endpoint {endpoint}") + + container.api_client.post = mock.Mock(side_effect=post) + return container.api_client.post + + +def test_parse_local_result_reads_equity_closes() -> None: + result = parse_local_result(create_result()) + + assert result.equity[0] == (START, 100000) + assert result.equity[-1] == (END, 100040) + + +def test_parse_local_result_skips_empty_points_and_reads_xy_points() -> None: + data = create_result() + data["charts"]["Strategy Equity"]["series"]["Equity"]["values"] = [[START, 1.0], [START + 60, None], + {"x": END, "y": 2.0}] - assert result.series("Indicators", "Fast EMA").points == [(START, 145.0), (END, 146.0)] + assert parse_local_result(data).equity == [(START, 1.0), (END, 2.0)] -def test_parse_backtest_result_reads_orders_sorted_by_id_with_readable_enums() -> None: - orders = parse_backtest_result(create_result()).orders +def test_parse_local_result_reads_orders_sorted_by_id_with_readable_enums() -> None: + orders = parse_local_result(create_result()).orders assert [o.id for o in orders] == [1, 2] assert (orders[0].type, orders[0].direction, orders[0].status) == ("Market", "Buy", "Filled") @@ -102,50 +145,45 @@ def test_parse_backtest_result_reads_orders_sorted_by_id_with_readable_enums() - assert orders[0].time == datetime(2013, 10, 7, 14, 10, tzinfo=timezone.utc) -def test_parse_backtest_result_reads_statistics_state_and_configuration() -> None: - result = parse_backtest_result(create_result()) +def test_parse_local_result_reads_statistics_status_and_configuration() -> None: + result = parse_local_result(create_result()) assert result.statistics["Sharpe Ratio"] == "1.028" assert result.runtime_statistics["Equity"] == "$99,839.57" - assert result.status == "Completed" + assert (result.status, result.finished, result.progress) == ("Completed", True, 1.0) assert result.parameters == {"delay-ms": "30"} assert result.start == datetime(2013, 10, 7, tzinfo=timezone.utc) -def test_parse_backtest_result_reads_cloud_backtests_with_list_orders() -> None: - data = create_result() - data["orders"] = list(data["orders"].values()) - del data["state"] - - result = parse_backtest_result(data) +def test_parse_local_result_of_a_running_backtest() -> None: + result = parse_local_result(create_result(status="Running")) - assert [o.id for o in result.orders] == [1, 2] - assert result.status == "" + assert (result.status, result.finished) == ("Running", False) + assert result.start is None -def test_progress_of_a_running_backtest_comes_from_its_last_equity_point() -> None: +def test_local_backtest_progress_uses_the_date_range_from_the_log_while_running() -> None: data = create_result(status="Running", equity_points=5) data["charts"]["Strategy Equity"]["series"]["Equity"]["values"] = \ data["charts"]["Strategy Equity"]["series"]["Equity"]["values"][:3] + output_dir = create_local_backtest(Path.cwd() / "Project", "2026-10-08_16-00-57", data) - progress = parse_backtest_result(data).progress - - assert 0.45 < progress < 0.55 - + result = LocalBacktest(output_dir).load() -def test_progress_of_a_completed_backtest_is_one() -> None: - assert parse_backtest_result(create_result()).progress == 1.0 + assert result.start == datetime(2013, 10, 7, tzinfo=timezone.utc) + assert 0.45 < result.progress < 0.55 def test_local_backtest_finds_its_result_file_from_its_config() -> None: - project_dir = Path.cwd() / "Project" - output_dir = create_local_backtest(project_dir, "2026-10-08_16-00-57", create_result()) + output_dir = create_local_backtest(Path.cwd() / "Project", "2026-10-08_16-00-57", create_result()) (output_dir / "9999-summary.json").write_text("{}") backtest = LocalBacktest(output_dir) assert backtest.result_file == output_dir / "1121419604.json" - assert backtest.load().statistics["Total Orders"] == "2" + assert backtest.load().name == "2026-10-08_16-00-57" + assert [o.id for o in backtest.load_orders()] == [1, 2] + assert backtest.created == datetime(2026, 10, 8, 16, 0, 57).astimezone(timezone.utc) def test_local_backtest_finds_its_result_file_without_a_config() -> None: @@ -164,6 +202,13 @@ def test_local_backtest_without_results_or_with_a_partially_written_file_loads_n (output_dir / "1121419604.json").unlink() assert backtest.load() is None + assert LocalBacktest(Path.cwd() / "Project" / "backtests" / "missing").load() is None + + +def test_local_backtest_reads_its_log() -> None: + output_dir = create_local_backtest(Path.cwd() / "Project", "2026-10-08_16-00-57", create_result()) + + assert LocalBacktest(output_dir).load_log()[0] == "2026-10-08T20:01:00Z TRACE:: Engine.Run(): start" def test_list_local_backtests_returns_newest_first() -> None: @@ -175,3 +220,83 @@ def test_list_local_backtests_returns_newest_first() -> None: assert names == ["2026-10-09_09-30-00", "2026-10-08_16-00-57", "2026-01-01_00-00-00"] assert list_local_backtests(Path.cwd() / "Missing") == [] + assert list_local_backtests(None) == [] + + +def test_parse_cloud_backtest() -> None: + result = parse_cloud_backtest(create_cloud_backtest(), {"Strategy Equity": create_equity_chart()}) + + assert result.name == "Logical Red Monkey" + assert (result.status, result.finished, result.progress) == ("Completed", True, 1.0) + assert result.statistics == {"Net Profit": "136.589%"} + assert result.parameters == {"roc_window": "150"} + assert result.equity[-1] == (END, 100040) + assert result.start == datetime(2013, 10, 7, tzinfo=timezone.utc) + + +def test_parse_cloud_backtest_while_running_uses_reported_progress() -> None: + backtest = create_cloud_backtest(status="Running", completed=False, progress=0.42) + backtest["statistics"] = [] + + result = parse_cloud_backtest(backtest, {}) + + assert (result.finished, result.progress) == (False, 0.42) + assert result.statistics == {} + assert result.equity == [] + + +def test_cloud_backtest_loads_the_backtest_and_only_its_equity_chart() -> None: + post = mock_cloud_api(create_cloud_backtest()) + + result = CloudBacktest(123, BACKTEST_ID).load() + + assert result.equity[-1] == (END, 100040) + chart_requests = [c for c in post.call_args_list if c.args[0] == "backtests/chart/read"] + assert len(chart_requests) == 1 + assert chart_requests[0].args[1]["start"] == int(datetime(2013, 10, 7, tzinfo=timezone.utc).timestamp()) + + +def test_cloud_backtest_without_a_chart_yet_loads_the_summary() -> None: + backtest = create_cloud_backtest(status="In Queue...", completed=False, progress=0) + backtest["charts"] = {} + post = mock_cloud_api(backtest) + + result = CloudBacktest(123, BACKTEST_ID).load() + + assert result.status == "In Queue" + assert result.equity == [] + assert all(c.args[0] != "backtests/chart/read" for c in post.call_args_list) + + +def test_cloud_backtest_pages_through_orders() -> None: + mock_cloud_api(create_cloud_backtest(), orders=250) + + orders = CloudBacktest(123, BACKTEST_ID).load_orders() + + assert [o.id for o in orders] == list(range(1, 251)) + + +def test_cloud_backtest_reads_the_last_log_lines() -> None: + mock_cloud_api(create_cloud_backtest(), logs=2500) + + lines = CloudBacktest(123, BACKTEST_ID).load_log() + + assert len(lines) == 2000 + assert (lines[0], lines[-1]) == ("line 500", "line 2499") + + +def test_list_cloud_backtests_returns_newest_first() -> None: + mock_cloud_api(create_cloud_backtest()) + + backtests = list_cloud_backtests(123) + + assert [b.name for b in backtests] == ["Newer", "Older"] + assert backtests[0].created == datetime(2026, 10, 8, 19, 45, 10, tzinfo=timezone.utc) + + +def test_sort_newest_first_mixes_local_and_cloud_backtests() -> None: + local = LocalBacktest(Path("backtests") / "2026-05-01_12-00-00") + cloud: List = [CloudBacktest(1, "a" * 32, "Old", datetime(2025, 1, 1, tzinfo=timezone.utc)), + CloudBacktest(1, "b" * 32, "New", datetime(2026, 10, 1, tzinfo=timezone.utc))] + + assert [b.name for b in sort_newest_first([local] + cloud)] == ["New", "2026-05-01_12-00-00", "Old"] From bdbc8e0bb0b37ccb5c0b09d792b272fee0d2ee3c Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 16:58:00 -0400 Subject: [PATCH 4/8] fix: span the equity chart over the whole backtest period The x axis now runs from the backtest's start to its end date when they are known, so a running backtest's equity grows from left to right instead of being stretched over the full width. Local backtests take the period from LEAN's log while running and cloud backtests from the API. Co-Authored-By: Claude Opus 5.5 (1M context) --- lean/ui/backtest_screen.py | 33 ++++++++++++++++++++++++++++----- tests/commands/test_ui.py | 14 ++++++++++++-- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/lean/ui/backtest_screen.py b/lean/ui/backtest_screen.py index eb646cac4..ba47134f1 100644 --- a/lean/ui/backtest_screen.py +++ b/lean/ui/backtest_screen.py @@ -12,7 +12,7 @@ # limitations under the License. from dataclasses import dataclass, field -from datetime import timezone +from datetime import datetime, timezone from typing import List, Optional, Sequence, Tuple from rich.table import Table @@ -74,19 +74,37 @@ class LiveRun: class EquityChart(PlotextPlot): - """The equity of a backtest over time, with dates on the x axis and dollar values on the y axis.""" + """The equity of a backtest over time, with dates on the x axis and dollar values on the y axis. + + When the backtest's period is known the x axis spans all of it, so a running backtest's equity grows from left + to right instead of being stretched over the full width. + """ def __init__(self, *, id: Optional[str] = None) -> None: super().__init__(id=id) self._points: Sequence[Tuple[float, float]] = [] + self._period: Optional[Tuple[float, float]] = None self._message = "No data" @property def points(self) -> Sequence[Tuple[float, float]]: return self._points - def set_points(self, points: Sequence[Tuple[float, float]], message: str = "No data") -> None: + @property + def x_range(self) -> Optional[Tuple[float, float]]: + """The times the x axis spans, or None without points.""" + if not self._points: + return None + low, high = self._points[0][0], self._points[-1][0] + if self._period is not None: + low, high = min(low, self._period[0]), max(high, self._period[1]) + return low, high + + def set_points(self, points: Sequence[Tuple[float, float]], message: str = "No data", + period: Optional[Tuple[datetime, datetime]] = None) -> None: + """Sets the equity points, and the start and end of the backtest if known.""" self._points = _downsample(points) + self._period = (period[0].timestamp(), period[1].timestamp()) if period is not None else None self._message = message self._replot() @@ -108,7 +126,10 @@ def _replot(self) -> None: y_ticks, y_step = value_ticks(min(ys), max(ys), max(3, min(8, self.size.height // 2))) plt.yticks(y_ticks, format_value_ticks(y_ticks, y_step, "$")) - x_ticks, x_labels = time_ticks(min(xs), max(xs), max(2, self.size.width // 16)) + x_low, x_high = self.x_range + if x_high > x_low: + plt.xlim(x_low, x_high) + x_ticks, x_labels = time_ticks(x_low, x_high, max(2, self.size.width // 16)) plt.xticks(x_ticks, x_labels) self.refresh() @@ -495,7 +516,9 @@ def _show_summary(self) -> None: def _show_chart(self) -> None: result = self._result message = "Waiting for results..." if result is None or not result.finished else "No equity data" - self.query_one("#equity", EquityChart).set_points(result.equity if result is not None else [], message) + period = (result.start, result.end) if result is not None and result.start and result.end else None + self.query_one("#equity", EquityChart).set_points(result.equity if result is not None else [], message, + period) def _show_statistics(self) -> None: table = self.query_one("#statistics-table", Static) diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index 220d7b50c..675613710 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -13,6 +13,7 @@ import asyncio import json +from datetime import datetime, timezone from pathlib import Path from unittest import mock @@ -390,8 +391,10 @@ async def test(app: LeanApp, pilot) -> None: def test_results_of_a_running_backtest_refresh_until_it_finishes() -> None: async def test(app: LeanApp, pilot) -> None: - output_dir = create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", - create_result(status="Running", equity_points=3)) + running = create_result(status="Running", equity_points=5) + equity = running["charts"]["Strategy Equity"]["series"]["Equity"] + equity["values"] = equity["values"][:3] + output_dir = create_local_backtest(Path.cwd() / "CSharp Project", "2026-10-08_16-00-57", running) await pilot.press("v") await _settle(app, pilot) @@ -399,6 +402,13 @@ async def test(app: LeanApp, pilot) -> None: assert screen.query_one("#summary").border_title.startswith("2026-10-08_16-00-57 (local): Running ") assert screen._poll_timer is not None + # The chart spans the whole backtest, so the partial equity only fills part of it + chart = screen.query_one("#equity") + period = (datetime(2013, 10, 7, tzinfo=timezone.utc).timestamp(), + datetime(2013, 10, 11, 23, 59, 59, tzinfo=timezone.utc).timestamp()) + assert chart.x_range == period + assert chart.points[-1][0] < (period[0] + period[1]) / 2 + 3600 + (output_dir / "1121419604.json").write_text(json.dumps(create_result())) screen._poll() await _settle(app, pilot) From 13ef08b3e75b8b8c984f2292f7d4566200f9142e Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 17:22:10 -0400 Subject: [PATCH 5/8] fix: load backtest orders a page at a time and wait for cloud data The orders tab loaded every order before showing any, which for a cloud backtest with millions of orders meant tens of thousands of requests. It now loads 100 orders when the tab opens and the next 100 as the cursor or view nears the end, showing "Orders (100 of 3,555)". While a backtest runs only orders placed since the last refresh are requested, and the orders are loaded again once it completes so their final status shows. The cloud chart, orders and log endpoints answer {"status": "loading"} while they prepare a backtest's data, which left the orders tab empty after a backtest completed. Those answers are now retried, and if the statistics count orders the endpoint does not return yet, loading them is retried too. Co-Authored-By: Claude Opus 5.5 (1M context) --- lean/ui/backtest_screen.py | 192 +++++++++++++++++++++++++++++++------ lean/ui/results.py | 47 +++++---- tests/commands/test_ui.py | 117 +++++++++++++++++++++- tests/test_ui_results.py | 61 ++++++++++-- 4 files changed, 363 insertions(+), 54 deletions(-) diff --git a/lean/ui/backtest_screen.py b/lean/ui/backtest_screen.py index ba47134f1..9bf5b44e4 100644 --- a/lean/ui/backtest_screen.py +++ b/lean/ui/backtest_screen.py @@ -22,6 +22,7 @@ from textual.binding import Binding from textual.containers import Vertical, VerticalScroll from textual.events import Key +from textual.message import Message from textual.screen import ModalScreen, Screen from textual.timer import Timer from textual.widgets import DataTable, Footer, Header, Input, OptionList, RichLog, Static, TabbedContent, TabPane @@ -30,8 +31,8 @@ from textual_plotext import PlotextPlot from lean.ui.projects import UIProject -from lean.ui.results import (BacktestResult, BacktestSource, Order, list_cloud_backtests, list_local_backtests, - sort_newest_first) +from lean.ui.results import (ORDERS_PAGE, BacktestResult, BacktestSource, Order, list_cloud_backtests, + list_local_backtests, sort_newest_first) from lean.ui.ticks import format_value_ticks, time_ticks, value_ticks # Most equity charts are a few hundred points, this keeps rendering fast for long high resolution backtests @@ -41,6 +42,13 @@ SUMMARY_STATISTICS = ["Equity", "Net Profit", "Return", "Probabilistic Sharpe Ratio", "Unrealized", "Holdings", "Fees", "Volume"] +# The next page of orders loads when the cursor or the scroll position is this many rows from the end +ORDERS_LOAD_MARGIN = 20 + +# Orders can be readable a little after a cloud backtest completes, until then loading them is retried +ORDERS_RETRY_SECONDS = 10 +ORDERS_RETRIES = 6 + # Statistics where a negative value is bad and a positive value is good SIGNED_STATISTICS = {"Net Profit", "Return", "Unrealized", "Compounding Annual Return", "Sharpe Ratio", "Sortino Ratio", "Alpha", "Expectancy", "Information Ratio", "Treynor Ratio"} @@ -135,6 +143,34 @@ def _replot(self) -> None: self.refresh() +class OrdersTable(DataTable): + """The orders of a backtest, which asks for more orders when the cursor or scroll position nears the end.""" + + class NearEnd(Message): + pass + + def watch_scroll_y(self, old_value: float, new_value: float) -> None: + super().watch_scroll_y(old_value, new_value) + self._check_near_end() + + def watch_cursor_coordinate(self, old_coordinate, new_coordinate) -> None: + super().watch_cursor_coordinate(old_coordinate, new_coordinate) + self._check_near_end() + + @property + def near_end(self) -> bool: + """Whether the cursor or the bottom of the view is close to the last row.""" + if self.row_count == 0: + return False + # Measured in rows, as the scroll limits are not updated yet when rows have just been added + last_visible_row = int(self.scroll_y) + self.size.height + return max(last_visible_row, self.cursor_row) >= self.row_count - ORDERS_LOAD_MARGIN + + def _check_near_end(self) -> None: + if self.near_end: + self.post_message(self.NearEnd()) + + class BacktestPicker(ModalScreen[Optional[int]]): """Lists a project's backtests, local and cloud, to pick one to show.""" @@ -231,10 +267,16 @@ def __init__(self, project: UIProject, logged_in: bool, live: Optional[LiveRun] self._backtests: List[BacktestSource] = [live.source] if live is not None else [] self._selected = 0 self._result: Optional[BacktestResult] = None - self._orders_loaded_for: Optional[str] = None self._log_loaded_for: Optional[str] = None self._poll_timer: Optional[Timer] = None + # Orders are loaded a page at a time as they are scrolled to + self._orders_for: Optional[str] = None + self._orders_count = 0 + self._orders_total: Optional[int] = None + self._orders_loading = False + self._orders_retries = 0 + def compose(self) -> ComposeResult: yield Header() with Vertical(): @@ -245,7 +287,7 @@ def compose(self) -> ComposeResult: with VerticalScroll(id="statistics-scroll"): yield Static(id="statistics-table") with TabPane("Orders", id="orders"): - yield DataTable(id="orders-table", cursor_type="row", zebra_stripes=True) + yield OrdersTable(id="orders-table", cursor_type="row", zebra_stripes=True) with TabPane("Logs", id="logs"): yield RichLog(id="log", markup=False, min_width=20) yield Footer() @@ -322,7 +364,7 @@ def action_show_tab(self, tab: str) -> None: self.query_one(TabbedContent).active = tab def action_reload(self) -> None: - self._orders_loaded_for = None + self._reset_orders() if not self._is_live(self.backtest): self._log_loaded_for = None self._load() @@ -370,10 +412,8 @@ def _is_live(self, source: Optional[BacktestSource]) -> bool: def _show_backtest(self) -> None: self._result = None - self._orders_loaded_for = None self._log_loaded_for = None - self.query_one("#orders-table", DataTable).clear() - self.query_one(TabbedContent).get_tab("orders").label = "Orders" + self._reset_orders() log = self.query_one("#log", RichLog) log.clear() if self._is_live(self.backtest): @@ -401,45 +441,46 @@ def _load(self) -> None: self._show_summary() return tab = self.query_one(TabbedContent).active - self._load_result(backtest, - load_orders=tab == "orders", - load_log=tab == "logs" and not self._is_live(backtest)) + self._load_result(backtest, load_log=tab == "logs" and not self._is_live(backtest)) @work(thread=True, exclusive=True, group="result") - def _load_result(self, backtest: BacktestSource, load_orders: bool, load_log: bool) -> None: + def _load_result(self, backtest: BacktestSource, load_log: bool) -> None: worker = get_current_worker() error = None - result = orders = log = None + result = log = None try: result = backtest.load() - if load_orders: - orders = backtest.load_orders() if load_log: log = backtest.load_log() except Exception as exception: error = exception if not worker.is_cancelled: - self.app.call_from_thread(self._show_result, backtest, result, orders, log, error) + self.app.call_from_thread(self._show_result, backtest, result, log, error) - def _show_result(self, backtest: BacktestSource, result: Optional[BacktestResult], orders: Optional[List[Order]], - log: Optional[List[str]], error: Optional[Exception]) -> None: + def _show_result(self, backtest: BacktestSource, result: Optional[BacktestResult], log: Optional[List[str]], + error: Optional[Exception]) -> None: if backtest is not self.backtest: return if error is not None: self.notify(f"Could not load the backtest: {error}", severity="error") + just_finished = False if result is not None: + just_finished = self._result is not None and not self._result.finished self._result = result self._apply_live_exit() + just_finished = just_finished and self._result.finished self._show_summary() self._show_chart() self._show_statistics() - if orders is not None: - self._show_orders(orders) - self._orders_loaded_for = backtest.id if log is not None: self._show_log(log) self._log_loaded_for = backtest.id + if just_finished: + # Orders loaded while running may have changed status, so load them again + self._reset_orders() + self._update_orders() + if self._should_poll(): self._poll_timer = self.set_timer(backtest.poll_seconds, self._poll) @@ -455,7 +496,6 @@ def _should_poll(self) -> bool: return self._is_live(self.backtest) and self._live.exit_code is None def _poll(self) -> None: - self._orders_loaded_for = None if not self._is_live(self.backtest): self._log_loaded_for = None self._load() @@ -465,11 +505,109 @@ def _tab_activated(self, event: TabbedContent.TabActivated) -> None: backtest = self.backtest if backtest is None: return - tab = event.pane.id - if (tab == "orders" and self._orders_loaded_for != backtest.id) or \ - (tab == "logs" and self._log_loaded_for != backtest.id): + if event.pane.id == "orders": + self._update_orders() + elif event.pane.id == "logs" and self._log_loaded_for != backtest.id: self._load() + # Loading orders a page at a time + + def _reset_orders(self) -> None: + self._orders_for = None + self._orders_count = 0 + self._orders_total = None + self._orders_retries = 0 + self.query_one("#orders-table", OrdersTable).clear() + self._label_orders() + + def _update_orders(self) -> None: + """Loads the first page of orders when the tab is shown, and new orders while the backtest runs.""" + backtest = self.backtest + if backtest is None or self.query_one(TabbedContent).active != "orders": + return + if self._orders_for != backtest.id: + self._reset_orders() + self._orders_for = backtest.id + self._load_more_orders() + elif self._result is not None and not self._result.finished: + # Ask for any orders the running backtest has placed since the last refresh + self._load_more_orders(new_orders=True) + + @on(OrdersTable.NearEnd) + def _near_end_of_orders(self) -> None: + # Checked again, as the message may have been posted while the table was still filling + if self.query_one("#orders-table", OrdersTable).near_end: + self._load_more_orders() + + def _load_more_orders(self, new_orders: bool = False) -> None: + """Loads the next page of orders, if there are more or new_orders asks to check for new ones.""" + backtest = self.backtest + if backtest is None or self._orders_for != backtest.id or self._orders_loading: + return + if not new_orders and self._orders_total is not None and self._orders_count >= self._orders_total: + return + self._orders_loading = True + self._label_orders() + self._load_orders_page(backtest, self._orders_count) + + @work(thread=True, group="orders") + def _load_orders_page(self, backtest: BacktestSource, start: int) -> None: + try: + orders, total = backtest.load_orders(start, start + ORDERS_PAGE) + error = None + except Exception as exception: + orders, total, error = [], None, exception + self.app.call_from_thread(self._add_orders, backtest, start, orders, total, error) + + def _add_orders(self, backtest: BacktestSource, start: int, orders: List[Order], total: Optional[int], + error: Optional[Exception]) -> None: + self._orders_loading = False + self._label_orders() + if backtest is not self.backtest or self._orders_for != backtest.id or start != self._orders_count: + # The table was reset while this page loaded + if self._orders_for == getattr(self.backtest, "id", None): + self._load_more_orders() + return + if error is not None: + self.notify(f"Could not load orders: {error}", severity="error") + return + + self._show_orders(orders) + self._orders_count += len(orders) + self._orders_total = max(total or 0, self._orders_count) + self._label_orders() + + if self._orders_total == 0 and self._orders_expected() and self._orders_retries < ORDERS_RETRIES: + self._orders_retries += 1 + self.set_timer(ORDERS_RETRY_SECONDS, self._retry_orders) + + def _orders_expected(self) -> bool: + """Whether the statistics count orders that the orders endpoint does not return yet.""" + result = self._result + if result is None or not result.finished: + return False + try: + return int(result.statistics.get("Total Orders", "0").replace(",", "")) > 0 + except ValueError: + return False + + def _retry_orders(self) -> None: + backtest = self.backtest + if backtest is not None and self._orders_for == backtest.id and self._orders_count == 0: + self._orders_total = None + self._load_more_orders() + + def _label_orders(self) -> None: + if self._orders_loading and self._orders_count == 0: + label = "Orders (loading...)" + elif self._orders_total is None: + label = "Orders" + elif self._orders_count < self._orders_total: + label = f"Orders ({self._orders_count:,} of {self._orders_total:,})" + else: + label = f"Orders ({self._orders_total:,})" + self.query_one(TabbedContent).get_tab("orders").label = label + # Rendering def _status(self) -> str: @@ -542,8 +680,7 @@ def _show_statistics(self) -> None: table.update(grid) def _show_orders(self, orders: List[Order]) -> None: - table = self.query_one("#orders-table", DataTable) - table.clear() + table = self.query_one("#orders-table", OrdersTable) for order in orders: table.add_row(str(order.id), order.time.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") if order.time else "", @@ -555,7 +692,6 @@ def _show_orders(self, orders: List[Order]) -> None: f"{order.value:,.2f}", order.status, order.tag) - self.query_one(TabbedContent).get_tab("orders").label = f"Orders ({len(orders)})" def _show_log(self, lines: List[str]) -> None: log = self.query_one("#log", RichLog) diff --git a/lean/ui/results.py b/lean/ui/results.py index 60394ac5f..40c0c957e 100644 --- a/lean/ui/results.py +++ b/lean/ui/results.py @@ -18,6 +18,7 @@ from datetime import datetime, timezone from json import loads from re import compile +from time import sleep from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -35,11 +36,15 @@ # Algorithm statuses after which a backtest's results no longer change, see Common/AlgorithmStatus.cs FINISHED_STATUSES = {"Completed", "RuntimeError", "Runtime Error", "Stopped", "Liquidated", "Deleted"} -# The cloud API returns at most this many orders and log lines per request -CLOUD_ORDERS_PAGE = 100 +# Orders are loaded a page at a time, the cloud API returns at most 100 orders and 200 log lines per request +ORDERS_PAGE = 100 CLOUD_LOG_PAGE = 200 MAX_LOG_LINES = 2000 +# How often and how long to wait for the cloud to prepare a backtest's chart, orders or logs +CLOUD_LOADING_ATTEMPTS = 30 +CLOUD_LOADING_DELAY_SECONDS = 1 + # Points to request of the equity chart from the cloud, which samples it down to this many CLOUD_CHART_POINTS = 1000 @@ -211,7 +216,8 @@ def load(self) -> Optional[BacktestResult]: """Loads the summary and equity chart, returns None if there are no results yet. Blocks.""" raise NotImplementedError() - def load_orders(self) -> List[Order]: + def load_orders(self, start: int, end: int) -> Tuple[List[Order], int]: + """Loads the orders from index start up to end, and returns them with the total number of orders.""" raise NotImplementedError() def load_log(self) -> List[str]: @@ -290,9 +296,10 @@ def _date_range_from_log(self) -> Tuple[Optional[datetime], Optional[datetime]]: break return None, None - def load_orders(self) -> List[Order]: + def load_orders(self, start: int, end: int) -> Tuple[List[Order], int]: result = self._last_result or self.load() - return result.orders or [] if result is not None else [] + orders = result.orders or [] if result is not None else [] + return orders[start:end], len(orders) def load_log(self) -> List[str]: log_file = self.path / "log.txt" @@ -326,9 +333,21 @@ def created(self) -> Optional[datetime]: return self._created def _post(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]: + """Makes a request about this backtest. + + The chart, orders and log endpoints prepare their data the first time it is asked for and answer + {"status": "loading", "progress": ...} until it is ready, so those answers are retried. + """ from lean.container import container - return container.api_client.post(endpoint, {"projectId": self.project_id, "backtestId": self.backtest_id, - **payload}) + + for attempt in range(CLOUD_LOADING_ATTEMPTS): + response = container.api_client.post(endpoint, {"projectId": self.project_id, + "backtestId": self.backtest_id, **payload}) + if response.get("status") != "loading": + return response + if attempt < CLOUD_LOADING_ATTEMPTS - 1: + sleep(CLOUD_LOADING_DELAY_SECONDS) + raise RuntimeError(f"QuantConnect is still preparing the data of {endpoint}, try again with r") def load(self) -> Optional[BacktestResult]: backtest = self._post("backtests/read", {})["backtest"] @@ -350,13 +369,9 @@ def load(self) -> Optional[BacktestResult]: return parse_cloud_backtest(backtest, charts) - def load_orders(self) -> List[Order]: - orders: List[Dict[str, Any]] = [] - while True: - page = self._post("backtests/orders/read", {"start": len(orders), "end": len(orders) + CLOUD_ORDERS_PAGE}) - orders.extend(page.get("orders") or []) - if not page.get("orders") or len(orders) >= page.get("length", 0): - return parse_orders(orders) + def load_orders(self, start: int, end: int) -> Tuple[List[Order], int]: + page = self._post("backtests/orders/read", {"start": start, "end": min(end, start + ORDERS_PAGE)}) + return parse_orders(page.get("orders") or []), page.get("length", 0) def load_log(self) -> List[str]: first = self._post("backtests/read/log", {"start": 0, "end": CLOUD_LOG_PAGE, "query": " "}) @@ -394,8 +409,8 @@ def created(self) -> Optional[datetime]: def load(self) -> Optional[BacktestResult]: return None - def load_orders(self) -> List[Order]: - return [] + def load_orders(self, start: int, end: int) -> Tuple[List[Order], int]: + return [], 0 def load_log(self) -> List[str]: return [] diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index 675613710..450fabebd 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -21,15 +21,17 @@ from textual.widgets import TabbedContent from lean.commands import lean +from lean.container import container from lean.ui import app as ui_app +from lean.ui import backtest_screen from lean.ui.actions import ACTIONS, CLOUD_BACKTEST_URL, format_command from lean.ui.app import ConfirmScreen, LeanApp, ProjectTable from lean.ui.backtest_screen import BacktestPicker, BacktestScreen from lean.ui.projects import UIProject, discover_local_projects, filter_projects, merge_cloud_projects from tests.test_helpers import create_api_project, create_fake_lean_cli_directory from lean.ui.results import CloudBacktest -from tests.test_ui_results import (BACKTEST_ID, create_cloud_backtest, create_local_backtest, create_result, - mock_cloud_api) +from tests.test_ui_results import (BACKTEST_ID, create_cloud_backtest, create_cloud_orders, create_local_backtest, + create_result, mock_cloud_api) def _discover() -> list: @@ -444,3 +446,114 @@ async def test(app: LeanApp, pilot) -> None: assert app.screen_stack[0].query_one(ProjectTable).row_count == 1 _run_app(test, logged_in=True) + + +async def _open_cloud_orders(app: LeanApp, pilot, backtest: dict, order_list: list) -> BacktestScreen: + """Shows a cloud backtest's orders tab, with the API serving order_list.""" + project = UIProject("Cloud Project", cloud_id=123) + post = mock_cloud_api(backtest, order_list=order_list) + post.side_effect = _only_backtest(post.side_effect) + app.push_screen(BacktestScreen(project, logged_in=True)) + await _settle(app, pilot) + await pilot.press("2") + await _settle(app, pilot) + return app.screen + + +def _only_backtest(post): + """Lists only the backtest under test, so the screen opens on it.""" + def answer(endpoint, payload): + if endpoint == "backtests/list": + return {"backtests": [{"backtestId": BACKTEST_ID, "name": "Logical Red Monkey", + "created": "2026-10-08 19:45:10"}]} + return post(endpoint, payload) + return answer + + +def _order_requests() -> list: + return [(c.args[1]["start"], c.args[1]["end"]) for c in container.api_client.post.call_args_list + if c.args[0] == "backtests/orders/read"] + + +def _orders_label(screen: BacktestScreen) -> str: + return str(screen.query_one(TabbedContent).get_tab("orders").label) + + +def test_orders_load_a_page_at_a_time_as_they_are_scrolled_to() -> None: + async def test(app: LeanApp, pilot) -> None: + screen = await _open_cloud_orders(app, pilot, create_cloud_backtest(), create_cloud_orders(250)) + table = screen.query_one("#orders-table") + + assert table.row_count == 100 + assert _orders_label(screen) == "Orders (100 of 250)" + assert _order_requests() == [(0, 100)] + + table.move_cursor(row=95) + await _settle(app, pilot) + assert table.row_count == 200 + + table.move_cursor(row=195) + await _settle(app, pilot) + assert table.row_count == 250 + assert _orders_label(screen) == "Orders (250)" + assert _order_requests() == [(0, 100), (100, 200), (200, 300)] + + # Every order is shown, so reaching the end again asks for nothing + table.move_cursor(row=249) + await _settle(app, pilot) + assert len(_order_requests()) == 3 + + _run_app(test, logged_in=True) + + +def test_orders_of_a_running_backtest_add_new_orders_on_refresh() -> None: + async def test(app: LeanApp, pilot) -> None: + order_list = create_cloud_orders(3) + running = create_cloud_backtest(status="Running", completed=False, progress=0.5) + screen = await _open_cloud_orders(app, pilot, running, order_list) + assert screen.query_one("#orders-table").row_count == 3 + + order_list.extend(create_cloud_orders(2, first_id=4)) + screen._poll() + await _settle(app, pilot) + + assert screen.query_one("#orders-table").row_count == 5 + assert _order_requests() == [(0, 100), (3, 103)] + + _run_app(test, logged_in=True) + + +def test_orders_reload_when_the_backtest_completes() -> None: + async def test(app: LeanApp, pilot) -> None: + running = create_cloud_backtest(status="Running", completed=False, progress=0.5) + screen = await _open_cloud_orders(app, pilot, running, create_cloud_orders(3)) + + running.update(create_cloud_backtest()) + screen._poll() + await _settle(app, pilot) + + assert screen.query_one("#orders-table").row_count == 3 + assert _order_requests()[-1] == (0, 100) + assert _orders_label(screen) == "Orders (3)" + + _run_app(test, logged_in=True) + + +def test_orders_that_are_not_readable_yet_are_retried() -> None: + async def test(app: LeanApp, pilot) -> None: + order_list = [] + backtest = create_cloud_backtest() + backtest["statistics"]["Total Orders"] = "2" + with mock.patch.object(backtest_screen, "ORDERS_RETRY_SECONDS", 0.1), \ + mock.patch.object(backtest_screen, "ORDERS_RETRIES", 1000): + screen = await _open_cloud_orders(app, pilot, backtest, order_list) + assert screen.query_one("#orders-table").row_count == 0 + + order_list.extend(create_cloud_orders(2)) + await pilot.pause(0.3) + await _settle(app, pilot) + + assert screen.query_one("#orders-table").row_count == 2 + assert _orders_label(screen) == "Orders (2)" + + _run_app(test, logged_in=True) diff --git a/tests/test_ui_results.py b/tests/test_ui_results.py index 61dac1b7a..b0bd8487c 100644 --- a/tests/test_ui_results.py +++ b/tests/test_ui_results.py @@ -17,6 +17,8 @@ from typing import Any, Dict, List, Optional from unittest import mock +import pytest + from lean.container import container from lean.ui.results import (CloudBacktest, LocalBacktest, list_cloud_backtests, list_local_backtests, parse_cloud_backtest, parse_local_result, sort_newest_first) @@ -94,9 +96,17 @@ def create_cloud_backtest(status: str = "Completed.", completed: bool = True, pr "parameterSet": {"roc_window": "150"}} -def mock_cloud_api(backtest: Dict[str, Any], orders: int = 0, logs: int = 0) -> mock.Mock: - """Makes the API client answer the backtest endpoints, paging orders and logs like the API.""" - order_list = [dict(create_orders()["1"], id=i + 1) for i in range(orders)] +def create_cloud_orders(count: int, first_id: int = 1) -> List[Dict[str, Any]]: + return [dict(create_orders()["1"], id=first_id + i) for i in range(count)] + + +def mock_cloud_api(backtest: Dict[str, Any], orders: int = 0, logs: int = 0, + order_list: Optional[List[Dict[str, Any]]] = None) -> mock.Mock: + """Makes the API client answer the backtest endpoints, paging orders and logs like the API. + + Pass order_list to change the orders the API returns while a test runs. + """ + order_list = order_list if order_list is not None else create_cloud_orders(orders) log_lines = [f"line {i}" for i in range(logs)] def post(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]: @@ -182,7 +192,8 @@ def test_local_backtest_finds_its_result_file_from_its_config() -> None: assert backtest.result_file == output_dir / "1121419604.json" assert backtest.load().name == "2026-10-08_16-00-57" - assert [o.id for o in backtest.load_orders()] == [1, 2] + assert backtest.load_orders(0, 100) == (backtest.load().orders, 2) + assert [o.id for o in backtest.load_orders(1, 100)[0]] == [2] assert backtest.created == datetime(2026, 10, 8, 16, 0, 57).astimezone(timezone.utc) @@ -268,12 +279,23 @@ def test_cloud_backtest_without_a_chart_yet_loads_the_summary() -> None: assert all(c.args[0] != "backtests/chart/read" for c in post.call_args_list) -def test_cloud_backtest_pages_through_orders() -> None: - mock_cloud_api(create_cloud_backtest(), orders=250) +def test_cloud_backtest_loads_one_page_of_orders_with_the_total() -> None: + post = mock_cloud_api(create_cloud_backtest(), orders=250) + + orders, total = CloudBacktest(123, BACKTEST_ID).load_orders(100, 200) + + assert [o.id for o in orders] == list(range(101, 201)) + assert total == 250 + assert post.call_count == 1 + + +def test_cloud_backtest_never_asks_for_more_than_a_page_of_orders() -> None: + post = mock_cloud_api(create_cloud_backtest(), orders=250) - orders = CloudBacktest(123, BACKTEST_ID).load_orders() + orders, _ = CloudBacktest(123, BACKTEST_ID).load_orders(0, 1000) - assert [o.id for o in orders] == list(range(1, 251)) + assert len(orders) == 100 + assert post.call_args.args[1]["end"] == 100 def test_cloud_backtest_reads_the_last_log_lines() -> None: @@ -300,3 +322,26 @@ def test_sort_newest_first_mixes_local_and_cloud_backtests() -> None: CloudBacktest(1, "b" * 32, "New", datetime(2026, 10, 1, tzinfo=timezone.utc))] assert [b.name for b in sort_newest_first([local] + cloud)] == ["New", "2026-05-01_12-00-00", "Old"] + + +def test_cloud_backtest_waits_while_the_api_prepares_the_orders() -> None: + post = mock_cloud_api(create_cloud_backtest(), orders=3) + serve = post.side_effect + answers = iter([{"status": "loading", "progress": 0.0, "success": True}] * 2) + post.side_effect = lambda endpoint, payload: next(answers, None) or serve(endpoint, payload) + + with mock.patch("lean.ui.results.CLOUD_LOADING_DELAY_SECONDS", 0): + orders, total = CloudBacktest(123, BACKTEST_ID).load_orders(0, 100) + + assert (len(orders), total) == (3, 3) + assert post.call_count == 3 + + +def test_cloud_backtest_gives_up_when_the_api_keeps_loading() -> None: + container.api_client.post = mock.Mock(return_value={"status": "loading", "progress": 0.5, "success": True}) + + with mock.patch("lean.ui.results.CLOUD_LOADING_DELAY_SECONDS", 0), \ + pytest.raises(RuntimeError, match="still preparing"): + CloudBacktest(123, BACKTEST_ID).load_orders(0, 100) + + assert container.api_client.post.call_count == 30 From a4edf3299d86cc4607afb04881d004930a8b8f08 Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 17:23:51 -0400 Subject: [PATCH 6/8] test: make the orders retry test deterministic The test added the orders while retries ran every 0.1 seconds, and waiting for the app's workers could last through all of them, so it sometimes failed. The fake endpoint now returns the orders from its third request. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/commands/test_ui.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index 450fabebd..d3b047373 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -470,6 +470,24 @@ def answer(endpoint, payload): return answer +class _OrdersAfter(list): + """Orders that the fake orders endpoint only returns from its request number `after` + 1 on.""" + + def __init__(self, after: int, orders: list) -> None: + super().__init__(orders) + self._after = after + + def __getitem__(self, index): + if isinstance(index, slice): + self._after -= 1 + if self._after >= 0: + return [] + return super().__getitem__(index) + + def __len__(self) -> int: + return super().__len__() if self._after < 0 else 0 + + def _order_requests() -> list: return [(c.args[1]["start"], c.args[1]["end"]) for c in container.api_client.post.call_args_list if c.args[0] == "backtests/orders/read"] @@ -541,19 +559,21 @@ async def test(app: LeanApp, pilot) -> None: def test_orders_that_are_not_readable_yet_are_retried() -> None: async def test(app: LeanApp, pilot) -> None: - order_list = [] backtest = create_cloud_backtest() backtest["statistics"]["Total Orders"] = "2" - with mock.patch.object(backtest_screen, "ORDERS_RETRY_SECONDS", 0.1), \ - mock.patch.object(backtest_screen, "ORDERS_RETRIES", 1000): - screen = await _open_cloud_orders(app, pilot, backtest, order_list) - assert screen.query_one("#orders-table").row_count == 0 - order_list.extend(create_cloud_orders(2)) - await pilot.pause(0.3) - await _settle(app, pilot) + # The endpoint returns no orders the first two times it is asked + order_list = _OrdersAfter(2, create_cloud_orders(2)) + with mock.patch.object(backtest_screen, "ORDERS_RETRY_SECONDS", 0.1): + screen = await _open_cloud_orders(app, pilot, backtest, order_list) + for _ in range(10): + if screen.query_one("#orders-table").row_count: + break + await pilot.pause(0.1) + await _settle(app, pilot) assert screen.query_one("#orders-table").row_count == 2 assert _orders_label(screen) == "Orders (2)" + assert len(_order_requests()) == 3 _run_app(test, logged_in=True) From b574558ac1c7015d4baae9ab3e37dc058eef6b9a Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 17:32:36 -0400 Subject: [PATCH 7/8] fix: don't count orders a running cloud backtest does not serve yet The cloud orders endpoint answers a running backtest with no orders and a length of 100, which showed as "Orders (0 of 100)". The length is now only used when a page has orders, and a running backtest without any shows "Orders (none yet)". Co-Authored-By: Claude Opus 5.5 (1M context) --- lean/ui/backtest_screen.py | 5 ++++- tests/commands/test_ui.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lean/ui/backtest_screen.py b/lean/ui/backtest_screen.py index 9bf5b44e4..7cbd54cfe 100644 --- a/lean/ui/backtest_screen.py +++ b/lean/ui/backtest_screen.py @@ -574,7 +574,8 @@ def _add_orders(self, backtest: BacktestSource, start: int, orders: List[Order], self._show_orders(orders) self._orders_count += len(orders) - self._orders_total = max(total or 0, self._orders_count) + # A running cloud backtest serves no orders yet but still reports a length, which is only trusted with orders + self._orders_total = max(total or 0, self._orders_count) if orders else self._orders_count self._label_orders() if self._orders_total == 0 and self._orders_expected() and self._orders_retries < ORDERS_RETRIES: @@ -602,6 +603,8 @@ def _label_orders(self) -> None: label = "Orders (loading...)" elif self._orders_total is None: label = "Orders" + elif self._orders_total == 0 and self._result is not None and not self._result.finished: + label = "Orders (none yet)" elif self._orders_count < self._orders_total: label = f"Orders ({self._orders_count:,} of {self._orders_total:,})" else: diff --git a/tests/commands/test_ui.py b/tests/commands/test_ui.py index d3b047373..e1b9a618c 100644 --- a/tests/commands/test_ui.py +++ b/tests/commands/test_ui.py @@ -577,3 +577,22 @@ async def test(app: LeanApp, pilot) -> None: assert len(_order_requests()) == 3 _run_app(test, logged_in=True) + + +def test_orders_of_a_running_cloud_backtest_ignore_the_length_without_orders() -> None: + async def test(app: LeanApp, pilot) -> None: + running = create_cloud_backtest(status="In Progress...", completed=False, progress=0.26) + screen = await _open_cloud_orders(app, pilot, running, []) + serve = container.api_client.post.side_effect + + # The cloud serves a running backtest's orders as an empty list with a length of 100 + container.api_client.post.side_effect = lambda endpoint, payload: \ + {"orders": [], "length": 100, "success": True} if endpoint == "backtests/orders/read" \ + else serve(endpoint, payload) + screen._poll() + await _settle(app, pilot) + + assert screen.query_one("#orders-table").row_count == 0 + assert _orders_label(screen) == "Orders (none yet)" + + _run_app(test, logged_in=True) From c97c83650a149dec38b3512ee457f3a1240d3978 Mon Sep 17 00:00:00 2001 From: Jared Broad Date: Thu, 8 Oct 2026 17:37:00 -0400 Subject: [PATCH 8/8] docs: add screenshots of the `lean ui` backtest results screen A completed and a running cloud backtest of the Intraday HMM Simplified Regime Detection project, for the pull request description. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/images/lean-ui-backtest-completed.png | Bin 0 -> 162693 bytes docs/images/lean-ui-backtest-running.png | Bin 0 -> 92471 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/lean-ui-backtest-completed.png create mode 100644 docs/images/lean-ui-backtest-running.png diff --git a/docs/images/lean-ui-backtest-completed.png b/docs/images/lean-ui-backtest-completed.png new file mode 100644 index 0000000000000000000000000000000000000000..7d0e3bcb9d28a52536d6edfca00ba7356e9d9ad2 GIT binary patch literal 162693 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

8HI`!c?vG2cMz)qZz_;&#SzMw(6eShg2< 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%0D6w!CAM0oW{ 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 mlR$RT^mOh!*esN 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