Z-Score Reversion
Bet on snap-back when the price wanders too far.
Z-score asks: how unusual is today's price compared to recent days? A z of -2 means 'cheaper than 97% of recent prices.' Most things drift back to average — so when the price gets weirdly cheap, buy; when it gets weirdly expensive, sell.
Z-score asks: how unusual is today's price compared to recent days? A z of -2 means 'cheaper than ~97% of recent prices'. A z of +2.5 means 'more expensive than 99% of recent prices'. Most things drift back to average — that's the trade. Pure stat-arb logic.
z = (price − μ) / σ over n bars
Real Z-Score Reversion bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.
This is the actual code the bot runs — not a re-explanation, not a simplified version. Whatever ships here is what executes when you press Run All in the workbench. Read it, copy it, fork it, build a better one.
const zscoreBot: BotDef = {
id: "zscore",
name: "Z-Score Reversion",
category: "stats",
glyph: "ζ",
tagline: "Bet on snap-back when the price wanders too far.",
formula: "z = (price − μ) / σ over n bars",
params: [
{ key: "period", label: "Window", kind: "number", default: 30, min: 10, max: 120, step: 1 },
{ key: "threshold", label: "|z| threshold", kind: "number", default: 2, min: 1, max: 3, step: 0.1 },
],
run: (ctx, p): BotResult => {
const period = Math.round(num(p, "period", 30));
const th = num(p, "threshold", 2);
const px = closes(ctx.candles);
const z = zscore(px, period);
const signals: Signal[] = [];
const sigArr: (1 | -1 | 0)[] = px.map(() => 0);
for (let i = 1; i < z.length; i++) {
const a = z[i - 1], b = z[i];
if (a == null || b == null) continue;
if (a < -th && b >= -th) {
signals.push({ i, kind: "buy", price: px[i], label: "reverting up" });
sigArr[i] = 1;
} else if (a > th && b <= th) {
signals.push({ i, kind: "sell", price: px[i], label: "reverting down" });
sigArr[i] = -1;
}
}
const bt = backtestLongOnly(px, sigArr);
const last = z[z.length - 1] ?? 0;
return {
signals,
metrics: [
{ key: "z", label: "Z", value: fmtNum(last, 2), tone: last > th ? "bear" : last < -th ? "bull" : "neutral" },
{ key: "thr", label: "Threshold", value: `±${th}`, tone: "neutral" },
{ key: "trades", label: "Trades", value: String(bt.trades) },
{ key: "win", label: "Win rate", value: `${(bt.winRate * 100).toFixed(0)}%`, tone: bt.winRate > 0.5 ? "bull" : "warn" },
],
pane: {
kind: "line",
series: [{ values: z, color: "var(--plasma)", label: "z" }],
refLines: [
{ value: th, color: "var(--bear)", label: "+" + th },
{ value: -th, color: "var(--bull)", label: "−" + th },
{ value: 0, color: "var(--fg-faint)" },
],
height: 80,
},
summary: `Z = ${last.toFixed(2)} (threshold ±${th}). ${signals.length} reversion entries.`,
beginner:
"Z-score asks: how unusual is today's price compared to recent days? A z of -2 means 'cheaper than 97% of recent prices'. Most things drift back to average — that's the trade.",
verdict: {
side: last < -th ? "buy" : last > th ? "sell" : "hold",
text: last < -th
? `${last.toFixed(2)}σ below mean — historical bounce zone.`
: last > th
? `${last.toFixed(2)}σ above mean — historical fade zone.`
: "Within the noise band — no signal.",
confidence: Math.min(1, Math.abs(last) / 3),
},
equity: bt.equity,
};
},
};id— unique key the workbench uses to find the bot.params— the sliders + inputs you see on the cell.run(ctx, p)— the function that gets called with candles + your params and returns the verdict.verdict— the BUY / SELL / HOLD pill at the top of the cell.metrics— the small stat boxes shown in the cell body.
- Copy the whole block above.
- On /quant, click + Import your bot in the bot library.
- Paste, hit save. It hot-loads into your workspace.
- Edit any param defaults or logic to your taste — it's now yours.
- ·Pair trading. Compute z-score of (asset A − asset B) and trade reversion to the mean spread.
- ·Mean-reverting equities. Utilities, consumer staples, fixed-income proxies tend to revert.
- ·Short timeframes. Z over 30 bars is a 6-week reversion clock for daily data — fast enough to act on.
- ·Trending markets. Z-score keeps reading 'unusual' as the trend continues; the bot fades the move and loses.
- ·Short windows. Z over 5-10 bars is essentially noise — the std-dev estimate is too unstable.
- ·Regime breaks. The mean and σ used for z-score are rolling estimates; in a regime shift they lag reality.
BUY when z just exited the lower threshold (was < -2, now ≥ -2). SELL when it exited the upper. Confidence is |z| / 3 capped at 1.0 — so z = -3 gives full conviction BUY.
Why ±2 thresholds and not ±1?+
Z-score of price vs z-score of returns — which?+
Catch oversold bounces, fade overbought spikes.
Volatility envelopes around a moving average.
Adaptive fair-value tracker. Smooth and self-correcting.
Linear regression line + std-deviation envelopes.
Is this market trending, mean-reverting, or random?