← Back to Blog
STRATEGY

Mean Reversion with Z-Score: A Quantitative Approach for Crypto

July 6, 2026 · 7 min read · LMEX.AI

Mean reversion is the bet that price, after stretching away from its average, tends to snap back. The z-score is the cleanest way to measure how far is far. It expresses the current price as a number of standard deviations from a rolling mean, which turns a vague idea, "this looks overextended", into a precise, backtestable threshold. This article builds a z-score reversion strategy for crypto perpetuals and is honest about the regime where it stops working.


What a z-score actually measures


The z-score standardises price against its own recent behaviour. A value of +2 means price sits two standard deviations above its rolling mean, statistically stretched. A value of -2 means the same on the downside. The strategy is symmetric: fade high z-scores by shorting, fade low z-scores by going long, and exit as the score returns toward zero.


import pandas as pd

def zscore(series: pd.Series, window: int = 20) -> pd.Series:
    mean = series.rolling(window).mean()
    std = series.rolling(window).std()
    return (series - mean) / std

The window length is the single most important choice. A short window makes the z-score twitchy and generates constant signals in noise. A long window makes it sluggish, so you fade moves that are actually the start of a real trend. There is no universal best value, only the value that survives a walk-forward test on your asset.


Turning the z-score into trades


The rules are deliberately simple: enter when the score exceeds an entry band, exit when it returns inside an exit band. Keeping entry and exit bands separate avoids flipping in and out on tiny oscillations around the threshold.


def zscore_positions(price, window=20, entry=2.0, exit=0.5):
    z = zscore(price, window)
    pos = pd.Series(0.0, index=price.index)
    pos[z > entry] = -1.0    # overextended up: short
    pos[z < -entry] = 1.0    # overextended down: long
    inside = z.abs() < exit
    pos[inside] = 0.0
    return pos.replace(0.0, pd.NA).ffill().fillna(0.0)

This is close cousin to Bollinger Band trading, which is really a z-score strategy with the bands drawn on the chart. If you want the indicator-library route instead of hand-rolling the stats, pandas-ta has z-score and Bollinger helpers built in.


The regime filter that saves the strategy


Here is the thing every mean-reversion trader learns the hard way: the strategy prints money in ranging markets and bleeds steadily in trending ones. When price is trending, a high z-score is not an overextension to fade, it is a strong trend to respect, and fading it means standing in front of the move.


So you do not trade z-score reversion blindly. You gate it with a trend filter and only take reversion signals when the market is actually ranging. A simple filter is the slope of a long moving average, or the ADX indicator: trade reversion only when trend strength is low.


def regime_ok_for_reversion(price, adx, adx_threshold=20):
    # only fade extremes when trend is weak (ranging)
    return adx < adx_threshold

Combine the two: take the z-score signal only when the regime filter says the market is ranging. In a trend, stand aside or switch to a momentum approach like EMA crossover. This one filter is the difference between a strategy that works and one that looks great until the first strong trend.


Costs and stops


Reversion trades are frequent and individually small, so fees matter. Prefer maker entries where you can. And because the whole thesis is "it will revert", you need a hard stop for when it does not, a level or a z-score extreme beyond which you admit the relationship broke and get out. Without that stop, one runaway trend turns a tidy strategy into an account event.


Frequently Asked Questions


Q: What window and thresholds should I start with?

A 20-bar window with entry at 2.0 and exit at 0.5 is a reasonable starting point on hourly data, but treat those as defaults to test, not truths. Walk-forward optimise them per asset and re-check periodically.


Q: Why did my mean-reversion bot lose money?

Almost always because it traded through a trend without a regime filter. Fading a strong trend is the classic reversion killer. Add a trend gate and only fade extremes in ranging conditions.


Q: Is z-score reversion better on which timeframe?

It works across timeframes, but ranging behaviour is more common intraday than on daily charts in crypto. Many traders run it on 15-minute to 4-hour bars. The key is matching your window to the timeframe.


Q: Can I use z-score on the spread between two assets instead of one price?

Yes, and that is exactly what pairs trading does. The z-score of a cointegrated spread is often more reliable than the z-score of a single price, because the spread is designed to be mean-reverting.


Related Articles


→ Bollinger Band Trading Strategy: Mean Reversion on LMEX Perpetuals
→ RSI Mean Reversion: A Deep Dive
→ pandas-ta: A Complete Indicator Library for Crypto Backtesting
← All ArticlesBuild a Bot →