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