from pathlib import Path
import qrt as qRunning LEAN backtests
q.bt.lean launches the local Lean CLI, keeps every native artifact, and connects the exact result JSON to QRT’s interactive report. This notebook demonstrates the complete workflow: initialize a workspace, run a backtest, inspect its lifecycle, save the report, and display the same report inline.
The cells are not executed automatically when the documentation is built because a LEAN backtest starts Docker. Run them interactively in a checkout with Lean CLI and Docker installed.
Locate the workspace
The adapter resolves paths before launch and does not change Python’s process-wide working directory. This helper makes the example work whether Jupyter starts in the repository root or in docs/bt.
repository = next(
path for path in (Path.cwd(), *Path.cwd().parents)
if (path / "pyproject.toml").is_file()
)
workspace = repository / "lean" / "demo"
workspacePosixPath('/home/hi/qrt/lean/demo')
Initialize a new workspace
q.bt.lean.init creates the directory when needed and synchronously runs lean init inside it. This is a one-time operation. The bundled demo workspace is already initialized, so the call is shown but commented out.
# q.bt.lean.init(
# workspace=repository / "lean" / "my-strategy",
# organization="my-organization",
# language="python",
# )Run a general Lean command
Pass only what normally follows the lean executable. General commands return the same asynchronous handle used by backtests.
version_run = q.bt.lean.run("--version", workspace=workspace)
version_result = version_run.wait()
print(version_result.stdout or version_result.stderr)lean 1.0.227
Launch the backtest
backtest returns immediately. Each call receives a unique directory below backtests, so another process cannot change which result belongs to this handle. update_image=False maps to Lean CLI’s --no-update option.
The adapter checks output-directory and Docker access before launch. It never accepts a sudo password. This notebook deliberately uses output_root=workspace / "qrt-backtests", creating a user-owned output tree instead of reusing a backtests directory that an earlier container may own.
Docker must be available without sudo to the user running Jupyter. Verify id includes the Docker socket group and docker version prints both Client and Server sections. After changing groups over Remote SSH, run Remote-SSH: Kill VS Code Server on Host…, close every connection to the host, reconnect, and start a new kernel. Closing only the local window can leave the old remote server and kernel running.
run = q.bt.lean.backtest(
workspace=workspace,
algorithm="demo_sma.py",
parameters={
"backtest-start": "2024-01-02",
"backtest-end": "2024-12-30",
"daily-universe-name": "sweden100",
"expected-daily-universe-members": "100",
},
update_image=False,
output_root=workspace / "backtests",
)
run.state<LeanRunState.RUNNING: 'running'>
stdout and stderr are captured incrementally, so they can be inspected while LEAN is still running:
print(run.stdout[-2_000:])
print(run.stderr[-2_000:])
Wait for completion. A timeout terminates the process and raises LeanTimeoutError; a non-zero exit raises LeanCommandError. Both exceptions retain their terminal result as .result.
result = run.wait(timeout=30 * 60)
result.state, result.result_path(<LeanRunState.SUCCEEDED: 'succeeded'>,
PosixPath('/home/hi/qrt/lean/demo/backtests/2026-07-27_19-30-15_69931148/1401111574.json'))
The result preserves the exact command, output directory, and every native file produced by LEAN:
print(result.specification.to_json())
print(f"Result: {result.result_path}")
print(f"Artifacts: {len(result.artifacts)}"){
"arguments": [
"backtest",
"/home/hi/qrt/lean/demo/demo_sma.py",
"--output",
"/home/hi/qrt/lean/demo/backtests/2026-07-27_19-30-15_69931148",
"--no-update",
"--parameter",
"backtest-start",
"2024-01-02",
"--parameter",
"backtest-end",
"2024-12-30",
"--parameter",
"daily-universe-name",
"sweden100",
"--parameter",
"expected-daily-universe-members",
"100"
],
"executable": "/home/hi/qrt/.venv/bin/lean",
"workspace": "/home/hi/qrt/lean/demo"
}
Result: /home/hi/qrt/lean/demo/backtests/2026-07-27_19-30-15_69931148/1401111574.json
Artifacts: 14
Save the HTML report
result.report reads this run’s exact JSON and writes a self-contained interactive HTML document. It also returns the BacktestReport, so saving and displaying do not require generating the report twice.
The bundled SMA algorithm records Asset Returns and Asset Weights chart series for invested symbols. q.bt.report discovers those series automatically and adds an expanding historical-performance treemap. Each slider frame includes every asset held up to that date; returns compound only while held and remain frozen at terminal performance after exit. A result created by an older version of the algorithm does not contain those inputs, so rerun the backtest before regenerating its report.
report_path = Path("sma-report.html").resolve()
report = result.report(
destination=report_path,
title="Sweden 20/100 SMA",
description="Synthetic XSTO universe strategy",
)
report_path, report.performance_treemap is not None(PosixPath('/home/hi/qrt/docs/bt/sma-report.html'), True)
Display the report in the notebook
display() embeds the self-contained report in an isolated iframe, preventing its styles from affecting the rest of the notebook. As with any JavaScript notebook output, the notebook must be trusted for the interactive charts to run.
The iframe dimensions can be customized when needed with: report.display(height=1200)
report.display()Report the latest existing run without launching LEAN
For analysis-only notebooks, point q.bt.report at the backtests directory. It selects the newest timestamped folder containing a completed LEAN result.
latest = q.bt.report(workspace / "backtests")
latest.save("latest-report.html")
latest.performance_treemap is not NoneTrue
Use run.cancel() to stop a running command. Cancellation is scoped to the CLI process owned by this handle; it does not search for or terminate unrelated LEAN workloads.