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:
| Price | Open interest | Regime | Typical reading |
|---|---|---|---|
| Up | Up | New longs | Fresh buying, trend supported by new positions |
| Down | Up | New shorts | Fresh selling, shorts pressing |
| Up | Down | Short covering | Rally driven by shorts leaving |
| Down | Down | Long unwind | Decline 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.
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:
let pricePct = (c - pc) / pc * 100.0
let oiPct = (o - po) / po * 100.0The 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.closebychart.closebefore 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.statand treat regime 4 as “liquidation flush” whenstats.sellLiqexceeds a USD threshold. - Higher timeframe regime. Subscribe to OHLCV and OI with
timeframe: 1hand classify inon htfOi.closeto colour a 5-minute chart by the hourly regime.
Related
- Pattern matching, functions
- Plots and panes for a plain OI change pane.
datareference,plotreference,scriptreference