Volume spike detector

View as MarkdownOpen the editor

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

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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).

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).
  • 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).

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