LIVE·
fetching live quotes from Yahoo Finance…
--:--:--UTC
Learn/Bots/Z-Score Reversion
Statisticalid · zscore
ζ

Z-Score Reversion

Bet on snap-back when the price wanders too far.

In plain English

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.

No jargon. Just what this bot does.
The longer version

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.

The math
formula
z = (price − μ) / σ over n bars
parameters
periodWindowrange 10 → 120default · 30
threshold|z| thresholdrange 1 → 3default · 2
Live demo

Real Z-Score Reversion bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.

symbolloading…
loading AMZN bars…
Source code · public

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.

lib/quant/bots.ts·lines 361425
TypeScript · MIT-licensed
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,
    };
  },
};
what each piece means
  • 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.
use this code yourself
  1. Copy the whole block above.
  2. On /quant, click + Import your bot in the bot library.
  3. Paste, hit save. It hot-loads into your workspace.
  4. Edit any param defaults or logic to your taste — it's now yours.
Specialty · when it shines, when it fails
✓ Shines when
  • ·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.
✗ Fails when
  • ·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.
How to read its verdict

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.

FAQ
Why ±2 thresholds and not ±1?+
Trade-off between fire-rate and precision. ±1 fires too often (every 16% of bars under normality). ±2 fires ~5% of bars and has higher precision per fire. Use ±2.5 if you want only the strongest reversion setups.
Z-score of price vs z-score of returns — which?+
We use price-z because the bot's purpose is reversion. Return-z is appropriate for measuring move sizes (how unusual is today's return?), which is a different question.