Mean reversion strategy in Python

Mean reversion bets that price extremes snap back toward an average, here is the full strategy, in Python, backtested with realistic costs.

What is mean reversion?

Mean reversion is the idea that prices oscillate around a fair value and that unusually large deviations tend to correct. Instead of chasing trends, a mean-reversion trader fades them: buying when an asset is stretched below its average and selling when it is stretched above.

It works best in range-bound, liquid markets and breaks down in strong trends, where 'cheap' keeps getting cheaper. The art is measuring 'stretched' robustly and cutting losers before a reversion that never comes.

The logic

We measure deviation with a rolling z-score: how many standard deviations the current price sits from its 50-bar mean. A z-score of -2 means price is two standard deviations below average, a statistically notable dip.

The rule is symmetric: go long when the z-score drops below -2, go short when it rises above +2, and stay flat in between. A stop-loss caps the damage when a 'cheap' market keeps falling.

mean reversion in Python

Here is the full strategy in the Manifold-BT expression DSL. It imports manifoldbt, builds the signal, configures realistic execution, and runs the backtest against Rust.

mean_reversion.py
import manifoldbt as mbt
from manifoldbt.indicators import close
from manifoldbt.helpers import time_range, Slippage, Interval

# Z-score of price vs its 50-bar moving average
z = close.zscore(50)

# Long 2σ below the mean, short 2σ above, flat in between
strategy = (
    mbt.Strategy.create("mean_reversion")
    .signal("z", z)
    .size(mbt.when(z < -2.0, 1.0, mbt.when(z > 2.0, -1.0, 0.0)))
    .stop_loss(pct=4.0)
)

start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
    universe={"binance": ["ETHUSDT"]},
    time_range_start=start,
    time_range_end=end,
    bar_interval=Interval.hours(1),
    initial_capital=10_000,
    fees=mbt.FeeConfig.binance_perps(),
    slippage=Slippage.fixed_bps(2),
    warmup_bars=50,
)
store = mbt.ingest(provider="binance", symbol="ETHUSDT", symbol_id=1,
                   interval="1h", start="2021-01-01T00:00:00Z",
                   end="2026-01-01T00:00:00Z")
result = mbt.run(strategy, config, store)
print(result.summary())
mbt.plot.tearsheet(result, show=True)

Backtest it with Manifold-BT

The config runs on hourly ETH bars with Binance perpetual fees and two basis points of slippage, so the result reflects real trading friction, not a frictionless ideal. Because mean reversion trades often, those costs matter a lot.

Tune the lookback window and z-score thresholds with a parameter sweep before trusting any single setting. A rule that only works at exactly z = -2 and a 50-bar window is overfit; one that works across a plateau of nearby settings is more likely to be real.

Backtest configuration
Universe{"binance": ["ETHUSDT"]}
Bar interval1h
FeesFeeConfig.binance_perps()
Slippagefixed 2 bps
Warmup50 bars
Sample tearsheet (illustrative, not a forecast)
Total return+38.1%
Sharpe1.21
Max drawdown-14.7%
Win rate57%
Trades412

Frequently asked questions

Does mean reversion work in crypto?

It works in range-bound regimes and struggles in strong trends. A trend filter or a stop-loss, as used here, is essential to survive the directional moves that punish pure reversion.

Keep reading

Run your first backtest

Install Manifold-BT and reproduce the backtest above in seconds. The Rust core runs years of bars sub-second so you can sweep parameters instead of waiting.

$pip install manifoldbt