---
title: "Volume spike detector"
description: "Flag bars whose volume blows past its trailing average, scored as a z-score against a rolling mean and standard deviation."
order: 69
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/volume-spike.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Volume spike detector

Flag bars whose volume blows past its trailing average, scored as a z-score against a rolling mean and standard deviation.

This recipe spots unusual volume. Instead of a fixed "alert above 1M" threshold that means nothing across different symbols, it measures how far the current bar's volume sits above its own recent average, in standard deviations. A reading of 3 means "three sigma above normal" whatever the symbol or timeframe. It is the cleanest possible introduction to the indicator loop: read one input, fold it into one class, write a few outputs.

## The wrun indicator

```typescript sample=cookbook-volume-spike
param("lookback", 50, { min: 10, max: 300, description: "Bars of history that define normal volume" });
param("threshold", 2.5, { min: 1, max: 6, description: "Z-score that counts as a spike" });
input("volume", ohlcv.volume); // the first input is the bar grid: the chart's own candles
output("z", line, lower, { color: "#64748b", width: 1, description: "Volume z-score" });
output("threshold", line, lower, { color: "#ef4444", width: 1 });
output("spike", shape, lower, { color: "#ef4444", shape_where: "is_spike" });
output("is_spike", none);

let zscore = new Zscore(50);
let threshold: f64 = 2.5;

function onStart(): void {
  zscore = new Zscore(i32(p_lookback()));
  threshold = p_threshold();
}

function onBar(): void {
  const z = zscore.update(bar.volume());
  if (isNaN(z)) return;
  out_z(z);
  out_threshold(threshold);
  out_spike(z);
  out_is_spike(z >= threshold ? 1.0 : 0.0);
}
```

## How it works

**The data.** One `input("volume", ohlcv.volume)` names the chart's own volume as the first input, the grid the run walks, and `bar.volume()` reads it in `onBar()`, one value per bar. That is the only feed the detector needs, and `onBar()` sees this bar's value and nothing else.

**The score.** A z-score answers "how surprising is this number?" You need two reference points: where volume usually sits, and how much it normally wobbles. `Zscore` is both reference points in one class: `update(x)` folds one bar into its window and returns `(x - mean) / stdev` over that window, or `0` when the window is flat. Because it is a ratio, it is comparable across any symbol or timeframe. A whale print on BTC and a thin altcoin both light up at the same number.

**The warm-up guard.** Until `lookback` bars have loaded, the class returns `NaN`, and `onBar()` returns before writing anything: every output stays `NaN` on that bar, and a `NaN` is not drawn, so the pane stays empty there instead of drawing noise ([Execution model](../core-concepts/execution-model.md)).

**The output.** `z` plots as a line in its own pane (`lower`). `threshold` is a line too, written to the same value on every bar, so you can see at a glance how close any bar is to firing. `spike` is a `shape` output whose value is the z-score (where the mark sits) and whose `shape_where` gate is the data-only `is_spike` output: the mark draws only on bars where the gate is `1`. Decisions are numbers here, and the sheet turns them into looks.

## Design notes

- The mark is two outputs: its position (`spike`) and the decision that gates it (`is_spike`). An output cannot gate itself, so the gate is its own `none` output.
- The threshold line is an output that never changes; its value is the param, written every bar.
- Settings take any value inside `min`..`max`; the `description` is the label the settings dialog shows beside the field.
- Every output has a value on every bar. After a Run the editor's Console prompt reads any of them (`last 20 z`, `is_spike`), and once the indicator is published, `z` can carry an alert set from the chart.

## Customize it

- **Sensitivity.** `threshold` is the main knob. Drop it toward `1.5` to catch milder bursts, raise it toward `4` to keep only genuine anomalies. The red line moves with it.
- **Memory.** `lookback` sets how much history "normal" is measured against. A short window (20) reacts to recent conditions and treats a busy session as the new baseline; a long window (200) compares against a calmer, broader average and flags more.
- **Two-sided.** As written, only high-volume bars fire because the gate is `z >= threshold`. To catch unusually quiet bars too, add a second gate output (`z <= -threshold`) and a second `shape` output gated by it.
- **Color and size.** The circle and line colors are hex strings on the declarations. `width` on the line output resizes it; `render.shape` picks a different mark kind than the chart's default ([Plotting](../presentation/plotting.md#text-labels-tables-strips-tints)).
- **Turn it into an alert.** Publish it, add it to a chart, and set an alert from the chart on `z` with **Crosses above** at 3. OpenMarket's alerts engine evaluates it in the cloud with the overlay's settings; **Once per bar** lets it fire again on later spikes, at most once per bar ([Alerts](../functions/alerts.md)).

## Run it

1. In the editor's Explorer, press **New indicator** and pick **Blank indicator**: the tab holds the `//@lang=wrun-ts` line alone.
2. Paste the indicator block above under that line and press **Run**.
3. At the editor's Console prompt, type `last 20 z` to read the score of the last 20 bars.

## Concepts used

- [Series functions](../functions/series-functions.md) and [TA library](../functions/ta-library.md) for `Zscore` over a trailing window
- [Setting kinds](../settings/kinds.md) for the `lookback` and `threshold` params
- [Missing values (NaN)](../core-concepts/na-and-scalar-types.md) for the `NaN` warm-up rule
- [Plotting](../presentation/plotting.md) and [Cards, frames and panels](../presentation/cards-frames-panels.md#marks-tints-and-fills) for the `line` outputs and the gated `shape`
