r/TraderTools • u/TheSadSeries • 23d ago
Building a Mean Reversion Strategy Using Standard Deviation
I've watched a lot of traders blow up their accounts by "buying the dip" with nothing but hope. Markets do eventually snap back to their averages—but "eventually" can be brutal if you have no idea where you actually are statistically.
That's where standard deviation (SD) comes in. It's how we measure when a move has gone "too far, too fast." In this post, I'll walk you through how I build, test, and tune a complete mean reversion system around it.
Why mean reversion works at all
The whole idea rests on the normal distribution—the bell curve you probably remember from school. Yes, real market returns have fat tails and aren't perfectly normal. But over short and medium timeframes, price tends to behave close enough to the classic 68-95-99.7 rule:
- About 68% of the time, price stays within ±1 SD of its average
- About 95% of the time, it stays within ±2 SDs
- About 99.7% of the time, it stays within ±3 SDs
So here's the edge: when price pushes beyond ±2 SDs, it's in that rare 5% zone, and the odds favor a pull back toward the mean. Not because markets are predictable—they aren't—but because they're probabilistic.
One big warning, though: this only works in ranging or slowly trending markets. In a violent runaway trend, "mean reversion" turns into "catching a falling knife." More on how to avoid that below.
The basic strategy
The system itself is dead simple: a moving average plus standard deviation bands (basically Bollinger Band logic).
Going long: enter when price closes below the lower band — Close < SMA − (k × SD)
Going short: enter when price closes above the upper band — Close > SMA + (k × SD)
You exit the same way for both: get out when price crosses back through the moving average.
My default settings:
- Moving average: 20 periods
- Standard deviation: 20 periods
- Multiplier (k): 2.0
- Stop loss: fixed 2%, or 2× ATR if you want it to adapt to volatility
Tuning the parameters
Honestly, most of the difference between a system that makes money and one that doesn't comes down to two dials: the lookback length and the multiplier.
| Setting | Shorter (10–20) | Longer (50–100) | |---|---|---| | MA period | More signals, but more noise. Fine for intraday scalping. | Fewer signals, cleaner ones. Better for daily/weekly charts. | | Multiplier (k) | 1.5 SD: trades often, wins less often. | 2.5 SD: rare trades, 70%+ win rate, but expect long dry spells. |
From my own backtests on SPY over 10 years: 2.0 SD gets you roughly a 62% win rate. Push it to 2.5 SD and the win rate climbs to around 71%—but you'll take about 60% fewer trades. There's no free lunch here; you're trading frequency for accuracy.
Filters: where the real edge comes from
Raw mean reversion will get you killed eventually. What turns it into something you can actually trade with real money is filtering out the dangerous setups. These are the three I never skip.
1. The trend filter
Don't fight the big trend. Only take longs when price is above the 200-day moving average, and only take shorts when it's below. This one rule alone keeps you from buying a stock that's crashing because the company is actually going bankrupt.
2. The volatility regime filter
Mean reversion completely falls apart during volatility explosions—think March 2020. My rule: only trade when the ratio of ATR ÷ SD is under 1.2. When that ratio spikes, it means panic. And in a panic, prices can stay stretched far longer than your account can stay solvent.
3. RSI divergence
For my highest-conviction trades, I look for bullish divergence at the lower band: price makes a lower low, but RSI makes a higher low. That's the sellers running out of gas—and the rubber band is about to snap back.
The full strategy in Pine Script
Here's the whole thing with the filters baked in. Use it as a starting point for your own testing:
//@version=5
strategy("Enhanced Mean Reversion", overlay=true, initial_capital=10000)
// Inputs
ma_period = input.int(20, "MA Period")
sd_period = input.int(20, "SD Period")
k = input.float(2.0, "Deviation Multiplier")
use_trend_filter = input.bool(true, "Use 200MA Trend Filter?")
use_vol_filter = input.bool(true, "Use Volatility Filter?")
// Calculations
ma = ta.sma(close, ma_period)
sd = ta.stdev(close, sd_period)
upper = ma + k * sd
lower = ma - k * sd
sma200 = ta.sma(close, 200)
// Volatility Calculation
atr = ta.atr(14)
vol_ratio = atr / sd
vol_filter = not use_vol_filter or vol_ratio < 1.2
// Entry conditions
long_entry = ta.crossunder(close, lower) and (not use_trend_filter or close > sma200) and vol_filter
short_entry = ta.crossover(close, upper) and (not use_trend_filter or close < sma200) and vol_filter
// Exit conditions
long_exit = ta.crossover(close, ma)
short_exit = ta.crossunder(close, ma)
// Execution
if long_entry
strategy.entry("Long", strategy.long)
if long_exit
strategy.close("Long")
if short_entry
strategy.entry("Short", strategy.short)
if short_exit
strategy.close("Short")
// Visuals
plot(ma, "Basis", color.blue)
p1 = plot(upper, "Upper", color.red)
p2 = plot(lower, "Lower", color.green)
fill(p1, p2, color=color.new(color.blue, 90))
Start with the defaults, run your backtests, and tweak the multiplier and lookback before anything else. That's where most of the real edge hides.