Backtest a mean reversion strategy (quick and easy)

June 14, 2025
Facebook logo.
Twitter logo.
LinkedIn logo.

Backtest a mean reversion strategy (quick and easy)

You can waste a year trading on instinct and still watch your edge slip away.

Most starting quants confuse mean reversion with guessing tops and bottoms. They chase every dip and spike, lose discipline, and end up with noisy results. It’s a lot more painful—and a lot less profitable—than it looks.

But you don’t have to do it the hard way.

A structured monthly mean reversion strategy, coded in Python, fixes this by enforcing discipline and keeping your process sharp. No more gut trades.

By reading today’s newsletter, you’ll get Python code to build systematic monthly mean reversion strategies that actually work in real markets.

Let’s go!

Backtest a mean reversion strategy (quick and easy)

Monthly mean reversion is a systematic investing approach that bets on extreme stock moves reverting to the average. It targets assets whose monthly returns deviate sharply, capitalizing on behavioral overreactions. By rebalancing once a month, you reduce emotion and overtrading while harnessing robust, repeatable returns.

Now, let’s look at the roots of this approach.

Mean reversion is rooted in decades-old market research showing prices often swing back to their long-term mean. Academics like Cliff Asness have popularized contrarian, quantitative investing that pushes beyond gut instinct. This idea stands in contrast to trend-following, leaning on behavioral finance and statistical edge.

Here’s how it fits into today’s trading landscape.

Professionals use monthly mean reversion to build disciplined, balanced portfolios that sidestep short-term noise. They apply quality screens, use volatility weighting, and automate trades to enforce unemotional execution. Risk controls and systematic rebalancing are the backbone, not just an afterthought.

Let’s see how it works in Python.

Imports and setup

These libraries provide tools for quantitative finance, data manipulation, and backtesting trading strategies.

1import pandas as pd
2import warnings
3
4from zipline import run_algorithm
5from zipline.pipeline.factors import Returns, VWAP
6from zipline.pipeline import CustomFactor, Pipeline
7from zipline.api import (
8    calendars,
9    attach_pipeline,
10    schedule_function,
11    date_rules,
12    time_rules,
13    pipeline_output,
14    record,
15    order_target_percent,
16    get_datetime
17)
18
19warnings.filterwarnings("ignore")

Our code uses these libraries to create a mean reversion trading strategy. We'll define custom factors, set up a pipeline for stock selection, and implement a trading algorithm.

Define our custom factor

We create a custom factor to calculate mean reversion scores for stocks.

1class MeanReversion(CustomFactor):
2    inputs = [Returns(window_length=21)]
3    window_length = 21
4
5    def compute(self, today, assets, out, monthly_returns):
6        df = pd.DataFrame(monthly_returns)
7        out[:] = df.iloc[-1].sub(df.mean()).div(df.std())

This code defines a custom factor called MeanReversion. It uses a 21-day window of stock returns to calculate a mean reversion score. The score is based on how far the most recent return is from the average, relative to the standard deviation. This helps identify stocks that may be overbought or oversold.

Set up our trading pipeline

We create a pipeline to select stocks based on our mean reversion factor.

1def compute_factors():
2    mean_reversion = MeanReversion()
3    vwap = VWAP(window_length=21)
4    pipe = Pipeline(
5        columns={
6            "longs": mean_reversion.bottom(5),
7            "shorts": mean_reversion.top(5),
8            "ranking": mean_reversion.rank(),
9        },
10        screen=vwap > 15.0
11    )
12    return pipe
13
14pipe = compute_factors()
15pipe.show_graph()

Our pipeline selects stocks for long and short positions based on their mean reversion scores. We choose the bottom 5 stocks for long positions and the top 5 for short positions. We also include a ranking of all stocks. The pipeline screens out low-priced stocks by only considering those with a 21-day volume-weighted average price above $15.

This code will help you visualize the pipeline.

Implement our trading algorithm

We define functions to handle trading logic and portfolio rebalancing.

1before_trading_start(context, data):
2    context.factor_data = pipeline_output("factor_pipeline")
3    record(factor_data=context.factor_data.ranking)
4
5    assets = context.factor_data.index
6    record(prices=data.current(assets, "price"))
7
8
9def rebalance(context, data):
10    factor_data = context.factor_data
11    assets = factor_data.index
12
13    longs = assets[factor_data.longs]
14    shorts = assets[factor_data.shorts]
15    divest = context.portfolio.positions.keys() - longs.union(shorts)
16
17    print(
18        f"{get_datetime().date()} | Longs {len(longs)} | Shorts {len(shorts)} | {context.portfolio.portfolio_value}"
19    )
20
21    exec_trades(data, assets=divest, target_percent=0)
22
23    exec_trades(
24        data, assets=longs, target_percent=1 / len(longs) if len(longs) > 0 else 0
25    )
26
27def exec_trades(data, assets, target_percent):
28    for asset in assets:
29        if data.can_trade(asset):
30            order_target_percent(asset, target_percent)
31
32
33def initialize(context):
34    attach_pipeline(compute_factors(), "factor_pipeline")
35    schedule_function(
36        rebalance,
37        date_rules.month_end(),
38        time_rules.market_open(),
39        calendar=calendars.US_EQUITIES,
40    )

These functions form the core of our trading algorithm. We gather factor data before trading starts, rebalance our portfolio at the end of each month, and execute trades to achieve our target allocations. The algorithm aims to go long on stocks with low mean reversion scores and short those with high scores.

Run our backtest

We set up and run a backtest of our trading strategy.

1start = pd.Timestamp("2020-01-01")
2end = pd.Timestamp("2024-07-01")
3capital_base = 25_000
4
5perf = run_algorithm(
6    start=start,
7    end=end,
8    initialize=initialize,
9    capital_base=capital_base,
10    before_trading_start=before_trading_start,
11    bundle="quotemedia",
12)

We run a backtest of our strategy from January 2020 to July 2024 with an initial capital of $25,000. The algorithm uses the Quotemedia data bundle for historical stock prices. After running the backtest, we plot the portfolio value over time to visualize the strategy's performance.

Your next steps

Zipline Reloaded is an excellent backtesting library. Check out the documentation to get it set up and running on your computer.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.