Event-Based Backtesting Factor Portfolios with Zipline Reloaded
6 min read
Core idea
A backtester that walks the clock forward, one bar at a time
Zipline Reloaded — the open-source fork of Quantopian's Zipline that survived the company's shutdown — is an event-driven backtesting framework. Where VectorBT computes signals over the whole price array in one shot, Zipline marches forward bar by bar, firing event hooks (before_trading_start, handle_data, scheduled functions) at the right moments and routing orders through commission and slippage models. The cost is speed: a Zipline backtest over a thousand symbols and five years might take minutes; the equivalent VectorBT sweep is seconds. The gain is fidelity: Zipline simulates the actual flow that a live strategy would experience — orders submitted at one time, filled at another, with a realistic slippage cost based on volume.
The cookbook's seventh topic implements two strategies — a momentum factor and a mean-reversion factor — using Zipline's Pipeline API to compute the factor across the whole universe, schedule a weekly rebalance, and execute orders to flip into the new long/short positions. Once you've followed both examples, the framework's vocabulary (context, data, pipeline_output, order_target_percent, schedule_function) becomes second nature.
The Pipeline API: declarative factor computation at universe scale
The single most important Zipline concept is the Pipeline. A Pipeline is a declarative computation graph that says: "for every asset in the universe, for every trading day, compute these factors and emit these columns." The engine handles the cross-sectional and time-series mechanics — windowed lookbacks, cross-asset alignment, screen application — so the user only writes the math.
A CustomFactor subclass defines inputs, window_length, and compute(). The Pipeline composes one or more factors with screens (e.g. "top 100 by dollar volume") and outputs columns (raw factor, top-N flag, bottom-N flag, rank). Each trading day, the engine evaluates the Pipeline and hands the result to your algorithm via pipeline_output("name"). The same definition that runs in a research notebook runs in a backtest runs (in principle) in production.
Why it matters
Order modelling and slippage are not optional in real markets
VectorBT's vectorized backtest assumes you can transact at the bar's closing price for the bar's volume, instantly, with a configurable percentage fee. Reality is messier: a market order for 100,000 shares of a small-cap moves the price; an iceberg-sized buy executes over hours; a limit order may never fill. Event-based backtesting is where slippage and commission models live. The cookbook's mean-reversion recipe wires in VolumeShareSlippage (an order can consume up to 0.25% of daily volume, and each order moves price by 1%) and PerShare commissions ($0.00075/share, $0.01 minimum). The Sharpe ratio you get with realistic costs is the only one worth quoting.
Path-dependent strategies need a real event loop
Some strategies depend on intra-bar state: trailing stops that lift as the price moves, position-sizing rules that respect open orders, rebalances that fire only when a portfolio metric crosses a threshold. These cannot be vectorized cleanly — they need the trader's algorithm to inspect context.portfolio mid-run and decide what to do. Zipline's event-loop model is what makes that possible.
Key takeaways
Mental model
The Zipline event loop
A backtest is a finite state machine driven by market-calendar events. Knowing which hook fires when is half the battle.
Pipelines, screens, and the asset universe
A Pipeline is composed of three things: factors (computations on data), screens (boolean masks that drop assets), and columns (the named outputs). The composition order matters more than it looks.
Momentum vs. mean-reversion — the same machinery, opposite signs
The cookbook's two strategies use identical Zipline scaffolding — same initialize, same rebalance, same exec_trades, same Pipeline shape — and differ only in:
- The factor definition: momentum is
(p[-21] - p[-252]) / p[-252] - (p[-1] - p[-21]) / p[-21]divided by 126-day volatility; mean-reversion is(monthly_return[-1] - mean(monthly_returns)) / std(monthly_returns)(the z-score). - The direction of selection: momentum goes long the top decile and short the bottom; mean-reversion goes long the bottom (oversold) and short the top (overbought).
This is the deep symmetry: the framework is generic; the strategy is two lines of factor math. Once you can build one, you can build any factor-portfolio strategy by swapping the CustomFactor and the long/short polarity. The mean-reversion recipe adds slippage and commission models — making the simulation more realistic — but the structural code is identical.
Practical application
A complete factor-portfolio backtest
The cookbook's pattern, distilled into a checklist:
- Install the bundle —
zipline ingest -b quandl(or your premium provider). This is a one-time step that materializes years of price data into Zipline's local bcolz store. - Define the factor —
class MyFactor(CustomFactor): inputs=[...]; window_length=N; def compute(self, today, assets, out, *inputs): out[:] = math. - Define the Pipeline —
make_pipeline()returns aPipeline(columns={"longs": factor.top(N), "shorts": factor.bottom(N), "rank": factor.rank()}, screen=liquidity_filter). - Wire
before_trading_start—context.factor_data = pipeline_output("factor_pipeline"). - Wire
initialize—attach_pipeline(...),set_commission(...),set_slippage(...),schedule_function(rebalance, date_rules.week_start(), time_rules.market_open()). - Write
rebalance(context, data)— readcontext.factor_data, compute the divest / longs / shorts sets, issueorder_target_percentcalls for each. - Optionally write
analyze(context, perf)— runs once at the end with the final performance DataFrame. - Call
run_algorithmwith start, end, initialize, before_trading_start, capital_base, bundle. Optionally passbenchmark_returnsfor alpha/beta auto-computation. - Pickle the result so downstream analysis (Pyfolio, Alphalens) doesn't require re-running the backtest.
Realistic costs — the two models to remember
For US equities, the cookbook's defaults are a reasonable starting point:
- Commission:
PerShare(cost=0.00075, min_trade_cost=0.01)— three-quarters of a cent per share, with a $0.01 minimum per trade. Maps roughly to retail brokers' agency rates. - Slippage:
VolumeShareSlippage(volume_limit=0.0025, price_impact=0.01)— your order can consume at most 0.25% of the bar's volume, and execution price moves 1% in your direction relative to the bar price.
If you're trading something less liquid (small-caps, individual options, foreign equities), these numbers should worsen substantially. The discipline: don't quote a Sharpe ratio that didn't subtract realistic costs.
Example
Consider a quant who wants to translate the previous topic's vector-based "moving-average crossover with volatility gating" study into an event-based backtest, this time across the full S&P 500 with realistic costs.
The translation:
- Define a
CustomFactorthat computes both signals:MA_Crossover(binary: isMA(5) > MA(20)?) andVolatilityGate(binary: is21d_volbetween 15% and 35%?). The factor'scompute()returns the product of the two — onlyTruewhen both signals fire. - Make a Pipeline with
columns={"signal": factor, "longs": factor & (factor.percentile_between(95, 100)), "rank": factor.rank()}andscreen=AverageDollarVolume(window_length=30).top(500). - In
initialize, attach the pipeline, set per-share commissions and volume-share slippage, schedule a weekly rebalance. - In
rebalance, readcontext.factor_data, compute the long set, exit anything no longer in the long set, enter anything new. Equal-weight usingorder_target_percent(asset, 1.0 / N_LONGS). - Run with
benchmark_returnsset to the SPY daily return series. - Analyse the
perfDataFrame — Zipline automatically populatesalpha,beta,sharpe,max_drawdowncolumns when a benchmark is provided.
The strategy that looked promising in VectorBT might disappoint here — slippage and commissions on a 500-stock universe rebalancing weekly are substantial. That's exactly the point of the event-based step: VectorBT identifies candidates; Zipline tells you which candidates survive contact with realistic costs.
Related lessons
Related concepts
- Event-Driven Architecturelinked concept
- Backtestinglinked concept
- Momentumlinked concept
- Mean Reversionlinked concept
- Slippagelinked concept