Skip to content
Open RaamTrade

ZScore — Z-Score

How many standard deviations the close sits from its rolling mean. The general-purpose mean-reversion primitive: every band is a special case of it.

Signature

ZScore(period, on=<source>)
Argument Type Required Notes
period int Param or literal Yes Lookback for the mean and standard deviation. Default 20, bounds 2–400.
on= symbol, OHLCV field, or indicator No Source. Defaults to the trade symbol’s close.

Formula

$$ z_t = \frac{C_t - \text{SMA}(C, n)_t}{\sigma(C, n)_t} $$

Population standard deviation over the same window as the mean.

Warmup

period bars; 0 until then. Also returns 0 on a perfectly flat window (zero standard deviation) — a flat market reads as “at the mean”, not as an extreme.

Reading the value

z.value
z.value_at(1)
z < -2
z.crossed_above(0)

Example

class ZReversion(Strategy):
"""Fade a two-sigma stretch; exit at the mean."""
period = Param(20, min=2, max=400)
entry = Param(2.0, min=0.5, max=4.0)
trade = Symbol()
sizing = FixedCapital()
def execute(self):
z = ZScore(self.period, on=self.trade)
if z.value < -self.entry and self.trade.is_flat:
self.buy(self.trade, reason=f"z {z.value:.2f}")
if z.value > 0 and self.trade.is_long:
self.sell(self.trade, self.trade.position, reason="Back at mean")

Variants

ZScore(20, on=self.trade.volume) # volume z-score — unusual activity detector

Notes

  • Standard statistics; the same standardisation Bollinger %B rescales.
  • Proven by an assert-via-trade run on SPY daily (matched a strategy-side mean/σ reference to 1e-4 on every bar; zero alarm trades).
  • Population (not sample) standard deviation, matching the platform’s stddev and Bollinger convention; sample-σ implementations read ~2–5% smaller |z| at short windows.