Skip to content

feature: add lean ui terminal UI with live backtest results - #675

Open
jaredbroad wants to merge 15 commits into
masterfrom
LEAN-CLI-UI
Open

jaredbroad wants to merge 15 commits into
masterfrom
LEAN-CLI-UI

Conversation

@jaredbroad

@jaredbroad jaredbroad commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Adds lean ui, a keyboard-driven terminal UI (in the style of btop) for browsing projects, running backtests and watching their results, local or in the cloud.

Completed cloud backtest of the Intraday HMM Simplified Regime Detection project:
lean-ui-backtest-completed

While a backtest runs, the equity chart fills in from left to right over the backtest's period and refreshes every 15 seconds:

Running cloud backtest at 42%:
lean-ui-backtest-running

What it does

  • Projects: lists local projects and, when logged in, the organization's cloud projects, marked local, cloud or synced. ↑↓/j k to move, / to filter, enter for actions.
  • b / c backtest locally / in the cloud and open the results right away. A local backtest is written to an output directory the UI picks so it can be read while running; a cloud backtest switches over as soon as lean cloud backtest prints its url. The command's output is the Logs tab while it runs.
  • v view results of every local and cloud backtest of the project, newest first. [ ] step through them, o opens a filterable list.
  • Results screen: runtime statistics, the Strategy Equity chart spanning the backtest's start to end, and Statistics, Orders and Logs tabs.
  • Orders load 100 at a time as they are scrolled to (Orders (100 of 3,555)), so backtests with millions of orders stay fast.
  • Push, pull and cloud status run after a confirmation, with their output in the UI. x stops a running command the way Ctrl+C would.

How it works

  • lean ui runs CLI commands as child processes (python -m lean), so their console output can't draw over the UI and no command needed refactoring.
  • Local result files and the cloud API share the same JSON format; lean/ui/results.py reads both. In the cloud only the Strategy Equity chart is read, with orders (100 per request) and logs (200 per request) loaded when their tab opens.
  • The chart, orders and log endpoints answer {"status": "loading"} while they prepare a backtest's data, and a running backtest's orders come back empty with a length of 100. Both are handled.
  • Textual is only imported by lean ui, so other commands start as before. textual and textual-plotext are added to install_requires (about 1.9 MB of wheels).

Testing

  • 2,700+ tests pass, including pilot-driven tests of the UI (tests/commands/test_ui.py), the result readers (tests/test_ui_results.py) and axis ticks (tests/test_ui_ticks.py). Static analysis passes.
  • Checked by hand against real data: a local backtest run from the UI over the sample data, refreshing while it ran, and 129 cloud backtests of the HMM project including paging through 3,555 orders.
  • Not yet done by hand: starting a cloud backtest with c from the UI end to end.

🤖 Generated with Claude Code

jaredbroad and others added 8 commits October 8, 2026 15:49
…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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…testing

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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@jaredbroad

Copy link
Copy Markdown
Member Author

🍿 #vibed

- Run commands with the lean-cli binary in the portable build
- Elevate `lean ui` with sudo on Linux so its commands need no terminal
- Claim the running command on the UI thread so only one runs at a time
- Treat null order fields as missing instead of crashing the orders tab
- Keep the selected backtest when a cloud backtest gets its id
- Store command output before the results screen is mounted
- Show command output in batches instead of a round trip per line
- Read cloud backtests through BacktestClient and QCBacktest
- Share the stored-credentials check with the whoami message
- Scope imports to the functions that use them
- Keep the new QCBacktest fields lenient, the API sends [] and "" for empty values
- Wait for the running command to stop before quitting instead of hanging on exit
- Mark the commands `lean ui` runs with LEAN_UI_COMMAND so cancelling a cloud run needs no prompt
  and Ctrl+Break only acts as Ctrl+C in them
- Honor a stop asked before the command has started
- Reuse QCLanguage for the project language labels
- Mark the live log loaded when a cloud backtest gets its id
- Evaluate each action's availability once per shown project
- Interrupt a command once per stop, force stop its containers on the second and kill it on the third
- Do not escalate a stop already asked for when quitting
- Stop containers gracefully on the first interrupt of a command `lean ui` runs
- Reuse QCBacktest.is_complete to tell when a cloud backtest has finished
- Read the output directory config through OutputConfigManager
- Name the output directory format once and use it everywhere
- Evaluate the `lean ui` command flag once in constants
- Scope the projects screen handlers to its own table and action list
- Stop polling a backtest whose result failed to load, ask to press r instead
- Treat a result file that cannot be read like one still being written
- Ask whether to delete a running cloud backtest when stopping it, instead
  of auto-confirming the cloud runner prompt
- Import single-use standard library names where they are used
- Move the `lean.ui` tests under tests/ui
- Cover the project table with a spinner until the first projects arrive
- Show "loading projects..." in the table subtitle from the start
- Focus the table once it has loaded, a loading widget cannot take focus
pyfakefs switches to the real filesystem process-wide for short moments,
so Textual's worker threads sometimes read the real disk instead of the
fake one. The two tests that build a screen outside the app now do so in
an event loop, which Python 3.9 needs for the lock every widget creates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants