Skip to main content

Scanning the universe

The scanner filters a list of candidate symbols down to the ones worth trading at a specific research clock. It runs a ScannerStrategy over each symbol's recent bars and keeps those that produce an actionable scan signal.

make scan
# or
uv run python main.py scan --scanner demo_volume --symbols NVDA,META,TSLA,AMD

Standalone scans default to wall-clock now. Pin them to a historical clock with --as-of:

uv run python main.py scan --scanner demo_volume --symbols NVDA,META,TSLA,AMD --as-of 2024-06-01

Example output:

SYMBOL SIGNAL
NVDA SCANNER_BUY
TSLA SCANNER_SELL

The demo scanner

The shipped demo_volume scanner flags a symbol when its latest bar shows unusually high volume (relative to its moving average) and a meaningful price move. It is pure pandas/numpy — see Scanners for the internals.

It is a demonstration, and it arrives the way your own scanner does: declared as an entry point by the engine's own package rather than hard-coded into a registry. On a bare install it is the only scanner there is.

Is the universe stable?

A config records the universe its scanner resolved, not the scanner. So if the scan moves, the book a deployment gets is not the book that was validated — and no promotion gate would notice, because the gates never see the scan twice.

uv run python main.py scan --scanner demo_volume --drift --as-of 2026-08-22
=== Scanner drift (61 flagged at 2026-08-22) ===
clock flagged overlap turnover
-1d 59 57 6.6%
-2d 62 55 19.7%
-5d 48 40 47.5%
Universe turnover peaks at 47.5% across these clocks.

Turnover is measured against the baseline universe, so "47.5%" reads as this share of the universe I am comparing from is not in the other one. A selection that turns over half its names across a week is a different object from one that is stable, and which you have decides how much a single validated run tells you.

This needs no new machinery: --as-of already resolves a scanner at an arbitrary historical clock, so drift is that seam asked several times and differenced.

How it feeds the other commands

backtest, optimize, walkforward, research, cache warm and live all accept a --scanner option. When set, they run the scanner first and trade only the flagged symbols; if the scanner flags nothing, they fall back to the candidate list. Pass --scanner none to skip scanning and use the symbols as-is.

The historical commands resolve the scanner at --end by default, so a validation window does not mix in today's universe, and --scan-as-of pins a different scanner clock. live is the exception, and deliberately so: it resolves at wall-clock now, because a live book is selected from the universe as it stands, not as it stood at the end of some window. It therefore takes no --scan-as-of.

Every scan reports the clock it resolved at, in the exchange zone — including the ones that resolved to "now", so a payload never leaves which universe it selected from as something to be inferred.

uv run python main.py backtest --scanner none --symbols AAPL,MSFT --start 2024-01-02 --end 2024-04-01