---
title: "Supply and demand zones"
description: "A zone at every confirmed swing high (supply, orange) and swing low (demand, sky), one zone height of ATR deep, anchored on the swing's own bar and extended…"
order: 76
section: "cookbook"
---

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

# Supply and demand zones

![Supply and demand zone boxes with nearest level price tags](/wrun/images/supply-demand-zones.png)

A zone at every confirmed swing high (supply, orange) and swing low (demand, sky), one zone height of ATR deep, anchored on the swing's own bar and extended right bar by bar until a close breaks through it. A broken zone fades to slate, stops extending, and a small rose mark sits on the breakout bar (under the bar that closed up through a supply zone, over the bar that closed down through a demand zone). At the right edge the nearest live zone above and below the close is tagged with its price: "S 84,210" above, "D 82,950" below (four decimals on sub-dollar markets: "S 0.1398").

The parts are the `PivotHigh`, `PivotLow` and `Atr` helpers from the TA library ([TA library](../functions/ta-library.md)), box handles for the zones and right-anchored label handles for the two price tags ([Drawing objects](../presentation/drawing-objects.md)), and two `shape` outputs for the breakout marks. This is also the `supply-demand-zones` template: the **Supply and Demand Zones** card under **On price** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=supply-demand-zones
// Supply and Demand Zones: a zone at every confirmed swing high (supply, orange) and swing low (demand, sky), extended right until a close
// breaks through it; a broken zone fades to slate and stops; a rose mark sits on the breakout bar; the nearest live zone above and below is tagged with its price.

param.int("swing_strength", 8, { min: 2, max: 30, label: "Swing strength in bars", description: "Bars on each side a swing high or low must beat" });
param.number("zone_atr", 0.25, { min: 0.05, max: 2, step: 0.01, label: "Zone height in ATR", description: "Zone height in ATR(14) measured at the swing" });
param.int("zones_per_side", 6, { min: 1, max: 12, label: "Zones per side", description: "Zones kept per side; the oldest is replaced" });
param.color("mark_color", "#fb7185", { label: "Break mark" }); // a colour setting: painted onto both break marks below by name, so the Style page shows one control for it
legend({ title: "({{swing_strength}})" }); // the words after the name in the legend: the swing strength, read by name
input("high", ohlcv.high); // swing highs come from the highs, swing lows from the lows
output("break_up", shape, overlay, { color: "@mark_color", description: "A mark at the bar's low on a bar that closed up through a supply zone" }); // a rose mark under the breakout bar, in the colour setting
output("break_down", shape, overlay, { color: "@mark_color", description: "A mark at the bar's high on a bar that closed down through a demand zone" }); // and over the breakdown bar
output("supply_near", none, overlay, { description: "Bottom of the nearest live supply zone above the close" }); // data-only, for the Console
output("demand_near", none, overlay, { description: "Top of the nearest live demand zone below the close" });
string("tag", { max_bytes: 48 }); // one bounded slot both tags are written through
handles.box({ opacity: 0.14, borderWidth: 1 }); // zones in chart time and price
handles.label({ anchor: "right", size: 11, align: "right" }); // the two price tags: pixels from the right edge, price on the y axis

const MAX_ZONES = 12; // per side: 24 boxes at most, ids 0..23
const RING = 64; // bars of history kept for the swing's own time and ATR (swing_strength is at most 30)
const ORANGE: i32 = rgba(248, 104, 0, 255); const SKY: i32 = rgba(56, 189, 248, 255); const SLATE: i32 = rgba(148, 163, 184, 255);
const TAG_X = 56.0; // the tags end 56 px from the pane's right edge, clear of the price-axis pills
function makeBoxes(first: i32, count: i32): BoxHandle[] { const out: BoxHandle[] = []; for (let i = 0; i < count; i += 1) out.push(draw.box(first + i)); return out; }
const supplyBoxes = makeBoxes(0, MAX_ZONES); const demandBoxes = makeBoxes(MAX_ZONES, MAX_ZONES); // handle objects allocate once
const supplyTag = draw.label(30); const demandTag = draw.label(31); const words = draw.label(32); // the one line of words while no swing has confirmed
// One zone per ring slot, per side: its top and bottom, its first bar, whether it is still live, and what changed this bar.
const sTop = new StaticArray<f64>(MAX_ZONES); const sBottom = new StaticArray<f64>(MAX_ZONES); const sStart = new StaticArray<f64>(MAX_ZONES);
const sLive = new StaticArray<i32>(MAX_ZONES); const sUsed = new StaticArray<i32>(MAX_ZONES); const sEvent = new StaticArray<i32>(MAX_ZONES); // event: 1 opened, 2 broke
const dTop = new StaticArray<f64>(MAX_ZONES); const dBottom = new StaticArray<f64>(MAX_ZONES); const dStart = new StaticArray<f64>(MAX_ZONES);
const dLive = new StaticArray<i32>(MAX_ZONES); const dUsed = new StaticArray<i32>(MAX_ZONES); const dEvent = new StaticArray<i32>(MAX_ZONES);
const times = new StaticArray<f64>(RING); const atrs = new StaticArray<f64>(RING); // the last bars' opens and ATRs, so a swing is anchored on its own bar

let strength = 8; let zoneAtr = 0.25; let zonesPerSide = 6;
let pivotHigh = new PivotHigh(8, 8); let pivotLow = new PivotLow(8, 8); const atr = new Atr(14);
let head = 0; let supplyCount = 0; let demandCount = 0; let bars = 0; let wordsShown = false; // the history ring's cursor, the zones seen per side, bars seen
let t: f64 = NaN; let prevT: f64 = NaN; let width: f64 = NaN;

// A price in words: thousands grouped, cents on small prices, four decimals under one.
function sbGrouped(n: i64): void { if (n >= 1000) { sbGrouped(n / 1000); sb_text(","); const r = n % 1000; if (r < 100) sb_text("0"); if (r < 10) sb_text("0"); sb_int(r); } else sb_int(n); }
function sbPrice(v: f64): void { const a = Math.abs(v); if (a >= 1000.0) sbGrouped(i64(Math.round(a))); else if (a >= 1.0) sb_f64(a, 2); else sb_f64(a, 4); }

// onStart() runs once before the first bar: read the settings and size the swing helpers.
function onStart(): void {
  strength = i32(p_swing_strength()); if (strength < 1) strength = 1; if (strength > RING - 2) strength = RING - 2;
  zoneAtr = p_zone_atr(); zonesPerSide = i32(p_zones_per_side()); if (zonesPerSide < 1) zonesPerSide = 1; if (zonesPerSide > MAX_ZONES) zonesPerSide = MAX_ZONES;
  pivotHigh = new PivotHigh(strength, strength); pivotLow = new PivotLow(strength, strength);
}
// onBar() runs once per bar: fold the bar into the helpers, open a zone on a confirmed swing, break zones the close went through, write the outputs;
// then open or fade the zones that changed, extend the live ones to this bar, and tag the nearest two on the live bar.
function onBar(): void {
  prevT = t; t = bar.time(); const high = bar.high(); const low = bar.low(); const close = bar.close(); bars += 1;
  if (!isNaN(prevT) && t > prevT && (isNaN(width) || t - prevT < width)) width = t - prevT;
  const atrNow = atr.update(high, low, close);
  times[head] = t; atrs[head] = atrNow; head = (head + 1) % RING;
  const swingHigh = pivotHigh.update(high); const swingLow = pivotLow.update(low); // reported on the bar that confirms them, strength bars after the swing
  for (let i = 0; i < MAX_ZONES; i += 1) { sEvent[i] = 0; dEvent[i] = 0; }
  if (isNaN(width)) return; // the first bar has no width yet
  const at = (head - 1 - strength + RING) % RING; // the swing's own bar in the ring
  let swingAtr = unchecked(atrs[at]); if (isNaN(swingAtr)) swingAtr = atrNow; // early in the run the ATR at the swing may not exist yet
  if (!isNaN(swingHigh) && !isNaN(swingAtr) && swingAtr > 0.0) { // a supply zone hangs down from the swing high
    let overlaps = false;
    for (let i = 0; i < MAX_ZONES; i += 1) if (unchecked(sUsed[i]) == 1 && unchecked(sLive[i]) == 1 && swingHigh <= unchecked(sTop[i]) && swingHigh >= unchecked(sBottom[i])) overlaps = true;
    if (!overlaps) {
      const k = supplyCount % zonesPerSide; supplyCount += 1;
      sTop[k] = swingHigh; sBottom[k] = swingHigh - zoneAtr * swingAtr; sStart[k] = unchecked(times[at]); sLive[k] = 1; sUsed[k] = 1; sEvent[k] = 1;
    }
  }
  if (!isNaN(swingLow) && !isNaN(swingAtr) && swingAtr > 0.0) { // a demand zone stands up from the swing low
    let overlaps = false;
    for (let i = 0; i < MAX_ZONES; i += 1) if (unchecked(dUsed[i]) == 1 && unchecked(dLive[i]) == 1 && swingLow <= unchecked(dTop[i]) && swingLow >= unchecked(dBottom[i])) overlaps = true;
    if (!overlaps) {
      const k = demandCount % zonesPerSide; demandCount += 1;
      dBottom[k] = swingLow; dTop[k] = swingLow + zoneAtr * swingAtr; dStart[k] = unchecked(times[at]); dLive[k] = 1; dUsed[k] = 1; dEvent[k] = 1;
    }
  }
  let breakUp: f64 = NaN; let breakDown: f64 = NaN; let supplyNear: f64 = NaN; let demandNear: f64 = NaN;
  for (let i = 0; i < MAX_ZONES; i += 1) {
    if (unchecked(sUsed[i]) == 1 && unchecked(sLive[i]) == 1 && unchecked(sEvent[i]) == 0) {
      if (close > unchecked(sTop[i])) { sLive[i] = 0; sEvent[i] = 2; breakUp = low; } // closed up through supply: spent, marked under the bar
      else if (unchecked(sBottom[i]) > close && (isNaN(supplyNear) || unchecked(sBottom[i]) < supplyNear)) supplyNear = unchecked(sBottom[i]);
    }
    if (unchecked(dUsed[i]) == 1 && unchecked(dLive[i]) == 1 && unchecked(dEvent[i]) == 0) {
      if (close < unchecked(dBottom[i])) { dLive[i] = 0; dEvent[i] = 2; breakDown = high; } // closed down through demand
      else if (unchecked(dTop[i]) < close && (isNaN(demandNear) || unchecked(dTop[i]) > demandNear)) demandNear = unchecked(dTop[i]);
    }
  }
  out_break_up(breakUp); out_break_down(breakDown); out_supply_near(supplyNear); out_demand_near(demandNear);
  const right = t + width;
  for (let i = 0; i < MAX_ZONES; i += 1) {
    if (unchecked(sUsed[i]) == 1) {
      const box = supplyBoxes[i]; const e = unchecked(sEvent[i]);
      if (e == 1 || unchecked(sLive[i]) == 1) box.set(unchecked(sStart[i]), unchecked(sTop[i]), right, unchecked(sBottom[i])); // a live zone grows to the newest bar
      if (e == 1) box.color(ORANGE).fill(ORANGE).opacity(0.14).border(1.0);
      if (e == 2) { box.set(unchecked(sStart[i]), unchecked(sTop[i]), right, unchecked(sBottom[i])).color(SLATE).fill(SLATE).opacity(0.04); } // spent: ends on the breakout bar
    }
    if (unchecked(dUsed[i]) == 1) {
      const box = demandBoxes[i]; const e = unchecked(dEvent[i]);
      if (e == 1 || unchecked(dLive[i]) == 1) box.set(unchecked(dStart[i]), unchecked(dTop[i]), right, unchecked(dBottom[i]));
      if (e == 1) box.color(SKY).fill(SKY).opacity(0.14).border(1.0);
      if (e == 2) { box.set(unchecked(dStart[i]), unchecked(dTop[i]), right, unchecked(dBottom[i])).color(SLATE).fill(SLATE).opacity(0.04); }
    }
  }
  if (bar.isLast() && supplyCount + demandCount == 0 && bars > 2 * strength + 20) { // a flat market (tied highs and lows never confirm a swing): say so instead of an empty pane
    if (!wordsShown) { sb_clear(); sb_text("No confirmed swings on this chart yet"); words.set(56, 14).text(str_tag_sb).anchor(ANCHOR_TOP_RIGHT).align(ALIGN_RIGHT).color(SLATE); wordsShown = true; }
  } else if (wordsShown) { words.delete(); wordsShown = false; }
  if (bar.isLast()) { // the two tags: the nearest live zone above and below the close, on the live bar only
    if (!isNaN(supplyNear)) { sb_clear(); sb_text("S "); sbPrice(supplyNear); supplyTag.set(TAG_X, supplyNear).text(str_tag_sb).color(ORANGE).size(11).anchor(ANCHOR_RIGHT).align(ALIGN_RIGHT); } else supplyTag.delete();
    if (!isNaN(demandNear)) { sb_clear(); sb_text("D "); sbPrice(demandNear); demandTag.set(TAG_X, demandNear).text(str_tag_sb).color(SKY).size(11).anchor(ANCHOR_RIGHT).align(ALIGN_RIGHT); } else demandTag.delete();
  }
}
```

## How it works

**A swing confirms late.** `swing_strength` (8, 2 to 30) is the number of bars on each side a swing high or low must beat, so a swing is known `swing_strength` bars after it happened; the zone is anchored back on the swing's own bar through `bar.time()`. A swing must beat its neighbours strictly.

**One zone height of ATR.** `zone_atr` (0.25, in ATR(14) measured at the swing) sets the zone's depth: a supply zone hangs from the swing high, a demand zone stands on the swing low. Every bar the live zones are extended right with `set()`; a close through a zone breaks it, the box fades to slate and stops extending, and the bar's low or high is written to `break_up` or `break_down`, the two rose `shape` outputs (NaN on every other bar, so nothing is drawn there).

**A ring per side.** `zones_per_side` (6, up to 12) zones stay per side; the oldest is replaced. The handle objects are made once, at module start.

**The nearest zone is data.** `supply_near` (the bottom of the nearest live supply zone above the close) and `demand_near` (the top of the nearest live demand zone below it) are data-only outputs, one value per bar, readable at the editor's Console prompt. The same two prices are the tags at the right edge, written through the one bounded slot with the allocation-free builder; the tag's decimals follow the price (four on sub-dollar markets).

## Where it runs

Every market with candles. The first zones appear once a swing has confirmed (about twice the swing strength in bars) and the ATR has warmed up (14 bars).

## When data is missing

A swing must beat its neighbours strictly, so a flat, tick-quantized market (a prediction market pinned at one price) may confirm none: the example then says "No confirmed swings on this chart yet" at the top-right instead of leaving the pane empty.

## Customize it

- **Bigger structures.** Raise `swing_strength`; the zones confirm later and there are fewer of them.
- **Deeper zones.** `zone_atr` up to 2 ATRs; a zone is broken by a close through its far edge.
- **Keep broken zones out.** Delete a broken zone's handle instead of fading it to slate, and its slot frees for the next swing.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Supply and Demand Zones** under **On price**.
2. Press **Run**. The first zones appear once a swing has confirmed and the ATR has warmed up.
3. At the editor's Console prompt, type `supply_near` and `demand_near` to read the two tagged prices on the newest bar.

## Concepts used

- [TA library](../functions/ta-library.md) for `PivotHigh`, `PivotLow` and `Atr`
- [Drawing objects](../presentation/drawing-objects.md) for box handles, right-anchored labels and chart coordinates
- [Plotting](../presentation/plotting.md) for `shape` outputs that draw only where they are finite
