---
title: "Positioning regimes"
description: "Each bar's open-interest change, counted in contracts and as a percent, in its own pane under the chart, colored by who moved: amber for price up with open…"
order: 83
section: "cookbook"
---

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

# Positioning regimes

![Open-interest regime histogram with key card and flip dots](/wrun/images/positioning-regimes.png)

Each bar's open-interest change, counted in contracts and as a percent, in its own pane under the chart, colored by who moved: amber for price up with open interest up (new longs), orange for price down with open interest up (new shorts), sky for price up with open interest down (short covering), violet for price down with open interest down (long closing). A key card at the top right of the chart names the four colors and gives each regime's share of the last `key_window` bars, so the colors explain themselves. On price, a small dot marks the bar where the window's leading regime changed hands: amber under the bar when buying pressure took over, orange over the bar when selling pressure did.

The parts are an `oi.close` input that reads NaN where the market has none and a `funding.rate_close` input that reads NaN off perpetuals ([Data sources](../core-concepts/data-sources.md)), one histogram output per regime so the legend names every colour ([Plotting](../presentation/plotting.md)), a `draw.card` with one text slot per value cell ([Cards, frames and panels](../presentation/cards-frames-panels.md)), and two `shape` outputs for the leader-flip dots. This is also the `positioning-regimes` template: the **Positioning Regimes** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=positioning-regimes
// Positioning Regimes: each bar's open-interest change in its own pane, colored by who moved. On a perpetual, open
// interest is counted in coins (the dollar reading over the bar's close), so a price move alone never changes it; a
// prediction market's reading is used as served (already a count of YES and NO pairs). Price up with OI up is
// new longs (amber), price down with OI up is new shorts (orange), price up with OI down is short covering (sky),
// price down with OI down is long closing (violet). A key card at the top right gives each regime's share of the
// last bars, so the colors explain themselves; a dot on price marks the bar where the window's leading regime changed
// hands and held for three bars, amber for buying pressure, orange for selling pressure.

param.int("smoothing", 1, { min: 1, max: 20, label: "Smoothing, bars", description: "Bars the OI change and the price change are averaged over before coloring; 1 = raw" });
param.int("key_window", 24, { min: 4, max: 200, label: "Key window, bars", description: "Bars the key card's shares are counted over" });
input("close", ohlcv.close); // the primary input: the chart's own candles define the grid every other input lines up on
input("oi", oi.close, { missing: "nan" }); // open interest at the bar's close, as served; a bar without a reading, or a market without open interest, reads NaN
input("funding", funding.rate_close, { missing: "nan" }); // served on perpetuals only (NaN elsewhere): their open interest is dollars, read in coins
output("new_longs", histogram, lower, { unit: "%", color: "#f8c000" }); // the bar's OI change in contracts as a percent, drawn by the regime that owns the bar: price up, OI up
output("new_shorts", histogram, lower, { unit: "%", color: "#f86800" }); // price down, OI up
output("short_covering", histogram, lower, { unit: "%", color: "#38bdf8" }); // price up, OI down
output("long_closing", histogram, lower, { unit: "%", color: "#a78bfa" }); // price down, OI down
output("regime", none); // data-only: 0 new longs, 1 new shorts, 2 short covering, 3 long closing; the Console can read it
output("share_new_longs", none); // data-only: each regime's share of the key window, 0..1, read in the Console
output("share_new_shorts", none);
output("share_short_covering", none);
output("share_long_closing", none);
output("buyers_lead", shape, overlay, { color: "#f8c000" }); // a dot under the bar where the window's leading regime turned to buying pressure (new longs or short covering)
output("sellers_lead", shape, overlay, { color: "#f86800" }); // a dot over the bar where it turned to selling pressure (new shorts or long closing)
output("leader", none); // data-only: the regime with the most bars in the window, 0..3
string("new_longs", { max_bytes: 8 }); // the card's value cells, one slot each, written on the live bar
string("new_shorts", { max_bytes: 8 });
string("short_covering", { max_bytes: 8 });
string("long_closing", { max_bytes: 8 });
string("window", { max_bytes: 16 });
string("text", { max_bytes: 40 }); // the one line of words when the market has no open interest
handles.label({ anchor: "top_right", align: "right", color: "#94a3b8", size: 11 }); // the notice, in pane pixels from the chart's top-right corner
draw.card("positioning_key", {
  title: "Positioning",
  anchor: "top_right",
  offset: [56, 0], // inward past the price-axis tags (High, Low, last price reach about 46 px into the pane)
  rows: [
    { label: "New longs", value: { text: "new_longs" }, color: "#f8c000" },
    { label: "New shorts", value: { text: "new_shorts" }, color: "#f86800" },
    { label: "Short covering", value: { text: "short_covering" }, color: "#38bdf8" },
    { label: "Long closing", value: { text: "long_closing" }, color: "#a78bfa" },
    { label: "Window", value: { text: "window" } },
  ],
});

const MAX_WINDOW = 200; // the ring is sized for the largest window; onStart() reads the one in use
const REGIMES = 4;
const regimeRing = new StaticArray<i32>(MAX_WINDOW); // the last window bars' regimes, -1 where the bar had no reading
const counts = new StaticArray<i32>(REGIMES); // how many bars of the window sit in each regime
const notice = draw.label(0);
let smoothing = 1; // settings, read in onStart()
let keyWindow = 24;
let oiChangeAvg = new Sma(1); // the smoothers, rebuilt in onStart()
let priceChangeAvg = new Sma(1);
let prevClose = NaN; // the previous bar's close and OI, as served
let prevOi = NaN;
let perp = false; // funding seen: a perpetual, whose dollar open interest moves with price
let oiSeen = false; // any finite OI reading at all: false means the market has no open interest
let head = 0; // the ring's write cursor and how many slots hold a bar
let filled = 0;
let leader = -1; // the regime holding the most bars of the window, once it has held for HOLD_BARS bars
let candidate = -1; // the regime leading right now, and how many bars in a row it has led
let candidateRun = 0;
const HOLD_BARS = 3; // a new leader is marked once it has led three bars in a row
let rangeAvg = NaN; // a running mean of high - low: the dot's distance from the bar

function share(index: i32): f64 { // a regime's share of the bars in the window that had a reading
  let counted = 0;
  for (let i = 0; i < REGIMES; i += 1) counted += counts[i];
  return counted == 0 ? NaN : f64(counts[index]) / f64(counted);
}

function sendShare(value: f64, send: () => i32): void { // "31%" into one card cell
  sb_clear();
  sb_f64(value * 100.0, 0);
  sb_text("%");
  send();
}

function onStart(): void {
  smoothing = i32(p_smoothing());
  keyWindow = i32(p_key_window());
  oiChangeAvg = new Sma(smoothing);
  priceChangeAvg = new Sma(smoothing);
  for (let i = 0; i < REGIMES; i += 1) counts[i] = 0;
}

// onBar() runs once per bar: the OI change in contracts as a percent, the price change, the regime from their signs, the window counts;
// then the histogram and the shares on every bar, the card's cells and the notice on the live bar only.
function onBar(): void {
  const close = bar.close();
  const high = bar.high();
  const low = bar.low();
  const oiNow = in_oi();
  const range = high - low;
  rangeAvg = isNaN(rangeAvg) ? range : rangeAvg + (range - rangeAvg) * 0.1;
  if (!isNaN(oiNow)) oiSeen = true;
  if (!isNaN(in_funding())) perp = true;
  const now = perp ? oiNow / close : oiNow; // both readings in one unit: coins on a perpetual, as served elsewhere
  const before = perp ? prevOi / prevClose : prevOi;
  const rawOiChange = isNaN(before) || before <= 0.0 ? NaN : ((now - before) / before) * 100.0;
  const rawPriceChange = isNaN(prevClose) ? NaN : close - prevClose;
  prevOi = oiNow;
  prevClose = close;
  const oiChange = oiChangeAvg.update(rawOiChange); // NaN until the smoothing window is full or while a reading is missing
  const priceChange = priceChangeAvg.update(rawPriceChange);
  let regime = -1;
  if (!isNaN(oiChange) && !isNaN(priceChange)) {
    if (oiChange >= 0.0) regime = priceChange >= 0.0 ? 0 : 1; // OI rising: new longs when price rose, new shorts when it fell
    else regime = priceChange >= 0.0 ? 2 : 3; // OI falling: short covering when price rose, long closing when it fell
  }
  const leaving = regimeRing[head]; // the bar that drops out of the window
  if (filled == keyWindow && leaving >= 0) counts[leaving] -= 1;
  regimeRing[head] = regime;
  if (regime >= 0) counts[regime] += 1;
  head = (head + 1) % keyWindow;
  if (filled < keyWindow) filled += 1;
  let shiftDot = NaN; // this bar's dot on price, NaN when the leader held
  let best = -1; // the window's leader: the regime with the most bars, holding at least two bars in five
  let bestCount = 0;
  let counted = 0;
  for (let i = 0; i < REGIMES; i += 1) {
    counted += counts[i];
    if (counts[i] > bestCount) {
      bestCount = counts[i];
      best = i;
    }
  }
  if (filled < keyWindow || counted == 0 || f64(bestCount) * 5.0 < f64(counted) * 2.0) best = -1; // no leader worth the name yet
  if (best == candidate) candidateRun += 1;
  else {
    candidate = best;
    candidateRun = 1;
  }
  if (candidate >= 0 && candidateRun == HOLD_BARS && candidate != leader) { // the lead changed hands and held: mark it once
    if (leader >= 0) shiftDot = candidate == 0 || candidate == 2 ? low - rangeAvg * 0.6 : high + rangeAvg * 0.6; // buying pressure below the bar, selling pressure above
    leader = candidate;
  }
  out_new_longs(regime == 0 ? oiChange : NaN); // one bar per regime output; the others read NaN and draw nothing
  out_new_shorts(regime == 1 ? oiChange : NaN);
  out_short_covering(regime == 2 ? oiChange : NaN);
  out_long_closing(regime == 3 ? oiChange : NaN);
  out_regime(regime < 0 ? NaN : f64(regime));
  const newLongs = share(0);
  const newShorts = share(1);
  const shortCovering = share(2);
  const longClosing = share(3);
  out_share_new_longs(newLongs);
  out_share_new_shorts(newShorts);
  out_share_short_covering(shortCovering);
  out_share_long_closing(longClosing);
  out_leader(leader < 0 ? NaN : f64(leader));
  out_buyers_lead(leader == 0 || leader == 2 ? shiftDot : NaN);
  out_sellers_lead(leader == 1 || leader == 3 ? shiftDot : NaN);
  if (bar.isLast()) {
    if (oiSeen && !isNaN(newLongs)) { // the card appears once every cell has a value; history bars never write them
      sendShare(newLongs, str_new_longs_sb);
      sendShare(newShorts, str_new_shorts_sb);
      sendShare(shortCovering, str_short_covering_sb);
      sendShare(longClosing, str_long_closing_sb);
      sb_clear();
      sb_int(keyWindow);
      sb_text(" bars");
      str_window_sb();
    } else if (!oiSeen) { // the market served no open interest at all: say so once, top right
      sb_clear();
      sb_text("No open interest on this market");
      notice.set(16, 14).text(str_text_sb);
    }
  }
}
```

## How it works

**Who moved is two signs.** On a perpetual, open interest arrives in dollars, so each reading is divided by the bar's close first: a dollar figure rises with price even when no position opened, which would paint short covering as new longs. Only a perpetual serves funding, so a finite funding reading is what marks one. A prediction market's open interest is its collateral, one dollar per YES and NO pair, already a count of contracts, so it is read as served. The bar's price change and its open-interest change in contracts, each averaged over `smoothing` bars (1 is raw), sort the bar into one of four regimes; `regime` carries the index (0 new longs, 1 new shorts, 2 short covering, 3 long closing) as a data-only output. The bar's open-interest change in contracts, as a percent, is written to the one histogram output that owns the regime (`new_longs`, `new_shorts`, `short_covering`, `long_closing`) and NaN to the other three, so each colour is its own series and the legend names it.

**The key card counts the window.** A ring of the last `key_window` (24, 4 to 200) regimes gives each regime's share, written to the card's text slots on the live bar (`share_*` carry the same fractions as data-only outputs). The card sits at the top right, offset inward past the price-axis tags.

**A leader flip is confirmed.** `leader` is the regime with the most bars in the window. The new leader must hold at least two bars in five of the window and stay in the lead for three bars in a row; on that bar `buyers_lead` (new longs or short covering took over) carries the bar's low and `sellers_lead` the bar's high, the two dots on price.

## Where it runs

Perps (Binance Futures, Hyperliquid), whose open interest is counted in coins, and prediction markets, whose open interest is the collateral as served (Polymarket serves it in sparse readings: most bars read NaN and the bars with a reading carry the whole change). The card appears once the window has readings.

## When data is missing

Spot, stocks and FX have no open interest: the pane stays empty, the card stays away, and one slate line of words sits at the top right of the chart: "No open interest on this market".

## Customize it

- **Smoother regimes.** Raise `smoothing` to average the two changes over more bars before sorting the bar.
- **A longer key.** `key_window` up to 200 bars; the ring is sized for the largest window.
- **Two colours instead of four.** Merge the two buying regimes and the two selling ones into two histogram outputs and a two-row card.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Positioning Regimes** under **Order flow**.
2. Press **Run** on a perp, such as BTCUSDT on Binance Futures: the pane fills with the four colours and the key card appears once the window has readings.
3. At the editor's Console prompt, type `last 24 regime` to read the regime index of the last 24 bars.

## Concepts used

- [Data sources](../core-concepts/data-sources.md) for the `oi` source and `missing: "nan"`
- [Plotting](../presentation/plotting.md) for one histogram output per colour
- [Cards, frames and panels](../presentation/cards-frames-panels.md) for `draw.card` and its text slots
