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.
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.
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
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:
- Win rate — trend: low (35–45%), few big winners. Reversion: high (60–70%), many small winners.
- Loss shape — trend: many small losses, occasional none. Reversion: rare but large losses when a "cheap" market keeps falling.
- Best regime — trend loves volatile, directional markets; reversion loves quiet, range-bound ones.
- Worst nightmare — trend hates chop; reversion hates a runaway trend that never comes back.
- Emotional cost — trend feels like losing constantly then winning big; reversion feels like printing money until one trade gives it all back.
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.