AlgoPro UniversityCourse home
Part 4 · Lesson

Mean-reversion strategies

The opposite bet: when price stretches too far from its average, it tends to snap back. Build a z-score and a Bollinger-bounce in Python, and weigh mean reversion against trend head-to-head.

Mean reversion is the philosophical opposite of trend following. Instead of betting that moves persist, you bet they overshoot — that price is a rubber band stretched around a fair value, and the further it stretches, the harder it snaps back. The edge is behavioural: fear and greed push price past what is justified, and liquidity providers profit by fading those extremes back toward the average.

Measuring "too far" with a z-score

To fade an extreme you first need to define "extreme" numerically. The z-score does exactly that: how many standard deviations is price from its recent average? A z of +2 means price is unusually high relative to its own recent behaviour; -2 means unusually low. It is a self-calibrating ruler that adapts as volatility changes.

pythonA z-score mean-reversion signal
import numpy as np
import pandas as pd

def zscore_reversion(df: pd.DataFrame, lookback: int = class="n">20,
                     entry: float = class="n">2.0, exit: float = class="n">0.5) -> pd.DataFrame:
    class="s">""class="s">"Fade extremes: short when price is 'entry' std above its mean,
    long when it is 'entry' std below, and close as it returns to the mean."class="s">""
    out = df.copy()
    ma = out[class="s">"close"].rolling(lookback).mean()
    sd = out[class="s">"close"].rolling(lookback).std()
    out[class="s">"z"] = (out[class="s">"close"] - ma) / sd          class="c"># standardised distance

    position = np.zeros(len(out))
    holding = class="n">0
    z = out[class="s">"z"].values
    for i in range(len(out)):
        if holding == class="n">0:
            if z[i] <= -entry:   holding = class="n">1      class="c"># too cheap -> buy
            elif z[i] >= entry:  holding = -class="n">1     class="c"># too rich  -> sell
        elif holding == class="n">1 and z[i] >= -exit:      class="c"># reverted -> close long
            holding = class="n">0
        elif holding == -class="n">1 and z[i] <= exit:      class="c"># reverted -> close short
            holding = class="n">0
        position[i] = holding

    out[class="s">"position"] = pd.Series(position, index=out.index).shift(class="n">1).fillna(class="n">0)
    return out

The two thresholds encode the whole trade plan: entry=2.0 says "only act on genuine extremes" and exit=0.5 says "take profit as price gets back near normal, do not wait for perfection". Waiting for the z-score to return all the way to zero looks greedy on paper and gives back profits in practice.

The Bollinger bounce

Bollinger Bands are the same idea in visual form: a moving average with an envelope drawn ±k standard deviations around it. When price tags the lower band it is ~2σ cheap; a "bounce" trade buys there and targets the middle band. It is a z-score strategy wearing a chart-friendly costume, and it is worth seeing both because you will meet both in the wild.

pythonBollinger-bounce entries and mean-target exits
def bollinger_bounce(df: pd.DataFrame, lookback: int = class="n">20, k: float = class="n">2.0):
    out = df.copy()
    mid = out[class="s">"close"].rolling(lookback).mean()
    sd  = out[class="s">"close"].rolling(lookback).std()
    out[class="s">"mid"], out[class="s">"upper"], out[class="s">"lower"] = mid, mid + k * sd, mid - k * sd

    class="c"># Buy when we close below the lower band; exit when we reach the mean.
    long_entry = out[class="s">"close"] < out[class="s">"lower"]
    long_exit  = out[class="s">"close"] >= out[class="s">"mid"]

    position = np.zeros(len(out)); holding = class="n">0
    le, lx = long_entry.values, long_exit.values
    for i in range(len(out)):
        if holding == class="n">0 and le[i]:  holding = class="n">1
        elif holding == class="n">1 and lx[i]: holding = class="n">0
        position[i] = holding
    out[class="s">"position"] = pd.Series(position, index=out.index).shift(class="n">1).fillna(class="n">0)
    return out
Fading extremes back to the mean
Price (blue) oscillating around its average (green) inside ±2σ Bollinger bands (violet). The strategy buys near the lower band and sells back at the mean — every dip toward the band is a candidate long.

Trend vs mean reversion, head to head

These two families are not just different tools — they are opposite worldviews, and they profit and suffer in exactly opposite conditions. A move that makes a trend follower rich is the same move that ruins a mean-reverter, and vice versa. Internalise this table and you will understand most of what separates good strategies from bad ones:

So far each strategy fires on its own. In reality a raw signal is too trusting — it trades in regimes it should sit out. Next we add filters that tell a strategy when to keep its mouth shut.