Recomputing indicators bar by bar is the most expensive thing an event-driven engine does: data.history(bar_count=200) on each of a thousand sessions re-reads and re-averages two hundred rows a thousand times. compute_signals moves that arithmetic out of the loop. The decisions — sizing, caps, rolls — stay in handle_data and go through the ordinary blotter, slippage and commissions.
The shape
The rules
context.universe is required — a dict {name: listing} set in initialize. The keys become the column names of the price panel and of the signals.
WARMUP at module level declares how many bars before the first session the panel includes. Declare it for the longest window: too short, and the indicator is still NaN when the run starts.
compute_signals is plain def. It receives prices, a panel of wide pandas frames — prices.close, prices.open, prices.high, prices.low, prices.volume — indexed by bar time with one column per name.
- Return a dict of pandas DataFrames (one column per name) or Series (one value per bar).
Reading signals
context.signals is cut to the simulation clock. At bar t there is no accessor that returns row t + 1.
Return the numbers and compare them in handle_data, rather than returning fast > slow. A comparison against NaN is False, so a boolean signal can’t tell warm-up from a genuine “no” — and is_ready can’t help.
Look-ahead check
Before the first order, the engine re-runs compute_signals on truncated history and compares. A rolling mean gives the same answer either way; a full-sample z-score, a shift(-1) or a division by the last value does not, and the run stops with LookAheadError:
The check compares at sampled cut points, so it catches every accidental leak in practice — full-sample normalisation and negative shifts move nearly every row. It is a smoke alarm, not a proof.
Ranking across a row (close.pct_change(63).rank(axis=1)) is causal: each bar’s rank uses only that bar’s values.
Datasets in signals
A point-in-time dataset mounted in initialize is reachable as prices.dataset(name), aligned to the same index, with known_at and age_days columns showing when each row was filed and how old it is. See Point-in-Time Datasets.
Sweeps
For hundreds of parameter variants, ziplime.vectorized.kernel.simulate_signals(prices, entries, exits, ...) runs signals through Ziplime’s own vectorized kernel with the same cost models, and the winner is replayed through the event-driven ledger with reconciliation after every bar. Futures and options are refused there. Install ziplime[numba] to compile the kernel’s inner loop.
Worked examples: examples/vectorized — signals_in_run.py runs the same strategy vectorized and bar by bar and checks them against each other trade for trade; sweep_then_report.py sweeps and replays.