FlowscopeDocs

OI regimes

Classify every bar by the direction of price and open interest, colour the OI change by regime and get alerted when a strong regime starts.

Price direction alone does not tell you who is acting. Open interest does: if price rises while open interest grows, new longs are opening. If price rises while open interest falls, shorts are closing. Combining the two directions gives four regimes that traders read every day:

PriceOpen interestRegimeTypical reading
UpUpNew longsFresh buying, trend supported by new positions
DownUpNew shortsFresh selling, shorts pressing
UpDownShort coveringRally driven by shorts leaving
DownDownLong unwindDecline driven by longs leaving or being liquidated

This script colours the open-interest change histogram by regime, tints the price background lightly, and shows how many bars in a row the current regime has lasted. Small moves below a threshold are classified as neutral, so noise does not flip the colours.

oi-regimes.fs
script "OI regimes"

input (
  minOiPct = input.float(0.05, title: "Min OI change (%)", min: 0.0, max: 5.0)
  minPricePct = input.float(0.02, title: "Min price change (%)", min: 0.0, max: 5.0)
  tintPrice = input.bool(true, title: "Tint price background")
  streakAlert = input.int(3, title: "Alert after bars in regime", min: 1, max: 20)
)

data (
  chart = subscribe(data.ohlcv)
  oi = subscribe(data.oi)
)

type RegimePayload {
  regime: string
  bars: int
  oiChangePct: float
}

state (
  regime: int = 0
  streak: int = 0
)

pane oiPane = pane(title: "OI change by regime", height: 0.25)

plot (
  oiBars = plot.histogram(title: "OI change %", on: oiPane)
  tint = plot.bg(title: "Regime tint")
)

alert regimeAlert = alert(title: "OI regime established", onClose: true, payload: RegimePayload)

fn regimeOf(pricePct: float, oiPct: float, minP: float, minOi: float) -> int {
  if math.abs(oiPct) < minOi || math.abs(pricePct) < minP {
    return 0
  }
  if pricePct > 0.0 {
    return oiPct > 0.0 ? 1 : 3
  }
  return oiPct > 0.0 ? 2 : 4
}

fn regimeColor(r: int) -> color {
  return match r {
    1 => color.green
    2 => color.red
    3 => color.cyan
    4 => color.amber
    _ => color.gray
  }
}

fn regimeName(r: int) -> string {
  return match r {
    1 => "New longs"
    2 => "New shorts"
    3 => "Short covering"
    4 => "Long unwind"
    _ => "Neutral"
  }
}

on chart.close {
  let c = chart.close
  let pc = chart.close[1]
  let o = oi.close
  let po = oi.close[1]
  let valid = c != null && pc != null && o != null && po != null && pc != 0.0 && po != 0.0

  if valid && c != null && pc != null && o != null && po != null {
    let pricePct = (c - pc) / pc * 100.0
    let oiPct = (o - po) / po * 100.0
    let r = regimeOf(pricePct, oiPct, minPricePct, minOiPct)

    streak = r == regime ? streak + 1 : 1
    regime = r

    let shade = regimeColor(r)
    oiBars.plot(oiPct, color: shade)
    if tintPrice && r != 0 {
      tint.plot(color: color.withAlpha(shade, 28))
    }
    script.setTitle(str.format("OI regimes: {0} ({1} bars)", regimeName(r), streak))

    if r != 0 && streak == streakAlert {
      regimeAlert.trigger(RegimePayload {
        regime: regimeName(r),
        bars: streak,
        oiChangePct: oiPct,
      })
    }
  }
}

How it works

Percent changes make the threshold portable

Open interest is measured in base units, so a raw change of 500 means something different on BTC than on a small coin. The script converts both price and OI changes into percent of the previous bar before classifying them:

Flowscope Script
let pricePct = (c - pc) / pc * 100.0
let oiPct = (o - po) / po * 100.0

The valid flag is false when any value is missing or a previous value is zero. Then nothing is plotted, so the bar shows a gap instead of a misleading zero. The if repeats the null checks next to valid, because a bool local does not narrow the values it was computed from.

Same timeframe, same bar

Both subscriptions follow the chart. When the chart bar closes, oi.close is the open interest of that same confirmed period and oi.close[1] the one before. The classification therefore always pairs a price bar with the OI bar of the same period. See data and update events for the full rules.

Classification in a plain function

regimeOf has no access to state and does not draw. It only maps two numbers to a regime code from 0 to 4, which makes it easy to read and to reuse. The colour and the name come from two match expressions. The expression form of match must be exhaustive, so each ends with a _ arm for neutral.

Streaks

Two state values remember the regime of the previous bar and how long it has lasted. The alert fires exactly once per regime run, on the bar where the streak reaches the configured length. script.setTitle keeps the current regime in the indicator’s legend row, which is useful when the pane is collapsed.

Background tint

plot.bg colours the full height of the price pane behind the bar. A bar it is not called for stays untinted, so neutral bars and the “off” setting both leave the background alone.

Variations

  • Open interest in USD. Multiply oi.close by chart.close before taking the change. On coins with fast price moves, the USD value moves with price, so base units usually classify more cleanly.
  • Confirm with liquidations. A long unwind with heavy long liquidations is forced selling rather than voluntary exit. Subscribe to data.stat and treat regime 4 as “liquidation flush” when stats.sellLiq exceeds a USD threshold.
  • Higher timeframe regime. Subscribe to OHLCV and OI with timeframe: 1h and classify in on htfOi.close to colour a 5-minute chart by the hourly regime.