Market data: footprint clusters, delta, open interest, order book
What a LootScript script can read besides candles: footprint clusters, delta, open interest, funding rate, recorded and live order book, time and sales, screener, other timeframes and instruments.
Besides candles, a LootScript script reads order flow data: footprint clusters, delta, open interest, funding rate, recorded order book history, live order book and time and sales, and screener metrics. You don’t need to enable anything: as soon as the script accesses, say, cluster.poc, the terminal subscribes to clusters for the chart’s instrument and timeframe — even if the chart shows regular candles. When the script is removed from the chart, the subscription is released.
This page covers what’s in each namespace, where the data comes from, which exchanges have it, and how to access values from another timeframe or instrument.
The main rule: no data means na, not 0
Any value on this page can be na. This happens when the exchange isn’t covered by the server, the timeframe isn’t supported, history is still loading, or the bar is older than the available history. Zero would mean “no trades happened”, which is a different fact: an accumulated sum with zeroes filled in looks plausible but is wrong.
Every namespace has an availability flag — cluster.available, delta.available, oi.available, and so on. Check it or use the na(x) function. Comparing to na with == won’t compile.
indicator("Accumulated delta without zero substitution", overlay = false)
var float cvd = 0.0
if delta.available {
cvd := cvd + delta.value
}
plot(delta.available ? cvd : na, "CVD", color = color.teal)The chart legend next to the script name hints why data is missing: “venue not covered”, “timeframe not supported”, “data loading…”, “no open order book”, “screener unavailable”.
Where each thing comes from
| Namespace | What it is | History on chart |
|---|---|---|
cluster.* |
Footprint clusters of a bar: volume by price | Yes, from the cluster server |
delta.* |
Delta of aggressive buys and sells | Yes |
oi.* |
Open interest | Yes |
funding.* |
Funding rate and its calculations | Yes |
depth.* |
Order book recorded by the server, per bar | Yes, limited depth |
book.* |
Live order book | Last bar only |
tape.* |
Live time and sales of trades | Last bar only |
screener.* |
Screener metrics for the instrument | Last bar only |
Clusters: cluster.*
Volume at each price level inside a bar, split into aggressive buys and sells — the same data drawn by the footprint chart. Levels are numbered from bottom to top, from 0 to cluster.levels - 1.
| Name | Returns |
|---|---|
cluster.available |
Whether clusters are available on this bar |
cluster.levels |
Number of price levels in the bar |
cluster.price(i) |
Price of level i |
cluster.bid(i) |
Aggressive buys at level i |
cluster.ask(i) |
Aggressive sells at level i |
cluster.total |
Total volume of the bar |
cluster.delta |
Buys minus sells across all levels |
cluster.poc, cluster.poc_index |
Price and index of the level with the highest volume |
cluster.max_cell |
Volume of the largest cell in the bar |
cluster.imbalance(i) |
Level skew: +1 buys, -1 sells, 0 none |
cluster.stacked_buy, cluster.stacked_sell |
Price where the longest run of skewed levels starts |
cluster.stacked_buy_steps, cluster.stacked_sell_steps |
Length of that run in levels |
cluster.value_area(pct) |
Lower and upper bounds of the value area; pct is the share of volume, 0.7 = 70% |
cluster.grid_step |
Price grid step of the bar |
Note the names: cluster.bid(i) is the buys that “lifted” the ask, not resting buy orders in the order book.
Clusters are available on timeframes from 30 seconds to one day.
indicator("POC and value area", overlay = true)
[vaLow, vaHigh] = cluster.value_area(0.7)
plot(cluster.poc, "POC", color = color.orange, style = plot.style_circles)
plot(vaHigh, "Area high", color = color.gray, style = plot.style_linebr)
plot(vaLow, "Area low", color = color.gray, style = plot.style_linebr)Delta: delta.*
Difference between aggressive buys and sells for a bar. Volume is in coins (base currency).
| Name | Returns |
|---|---|
delta.value |
Bar delta: buys minus sells |
delta.buy, delta.sell |
The two components separately |
delta.cum |
Cumulative delta from the start of loaded history |
delta.min, delta.max |
Minimum and maximum of running delta within the bar |
delta.open, delta.high, delta.low, delta.close |
Bar delta as a candle |
delta.trades |
Number of trades in the bar |
delta.usd |
Delta in quote currency, based on actual trade amounts |
delta.available |
Whether delta is available on this bar |
delta.min and delta.max can’t be computed from clusters — that’s the intra-bar delta path, calculated by the server. They show that buyers led the bar, but sellers closed it. delta.usd is more precise than delta.value * close: on a sharp bar, trade prices differ significantly from the close.
delta.cum starts at zero on the first loaded bar, so its level depends on the depth of loaded history. Compare changes, not absolute values.
Delta is available on timeframes from 30 seconds.
indicator("Delta and intra-bar reversal", overlay = false)
d = delta.value
reversed = delta.available and d < 0 and delta.max > -d
plot(d, "Delta", style = plot.style_columns, color = d >= 0 ? color.teal : color.red)
plotshape(reversed, "Reversal", style = shape.xcross, location = location.top, color = color.orange, size = size.tiny)Open interest: oi.*
| Name | Returns |
|---|---|
oi.open, oi.high, oi.low, oi.close |
Open interest for the bar in the units the exchange publishes |
oi.coins |
Open interest in coins |
oi.usd |
Open interest in quote currency |
oi.change |
Change in oi.close from the previous bar |
oi.available |
Whether data is available on this bar |
Units of oi.open…oi.close differ across exchanges, so for cross-exchange comparisons use oi.coins or oi.usd. Open interest comes on a minute grid: on second-based timeframes all values are na.
indicator("Price and open interest", overlay = false)
dp = ta.change(close)
build = oi.available and dp > 0 and oi.change > 0
plot(oi.change, "OI change", style = plot.style_columns, color = build ? color.teal : color.gray)Funding rate: funding.*
| Name | Returns |
|---|---|
funding.rate |
Predicted rate for the next settlement |
funding.interval_hours |
Hours between settlements |
funding.settled |
true on the bar where settlement occurred |
funding.settled_rate |
Rate actually charged on this bar; if there are multiple settlements in a bar, their sum |
funding.cum |
Sum of charged rates over loaded history |
funding.available |
Whether the instrument has funding |
The rate is a fraction, not percent: 0.0001 means 0.01%. The chart shows percent; scripts don’t convert. For backtesting, use funding.settled_rate: funding.rate is a prediction and changes until settlement. Spot has no funding, and on second-based timeframes values are na.
indicator("Funding per settlement, %", overlay = false)
plot(funding.rate * 100, "Rate prediction", style = plot.style_area, color = funding.rate >= 0 ? color.teal : color.red)
plotshape(funding.settled, "Settlement", style = shape.circle, location = location.bottom, color = color.orange, size = size.tiny)Recorded order book: depth.*
The order book as the server recorded it — the same data drawn by the heatmap — laid out per bar. Unlike live book.*, these values exist on every recorded bar. Prices are grouped into buckets; each bucket’s volume is the maximum during the bar, in coins.
| Name | Returns |
|---|---|
depth.available |
Whether the order book is recorded for this bar |
depth.levels |
Number of non-empty buckets in the bar |
depth.price(i) |
Lower bound of bucket i, ascending |
depth.qty(i) |
Maximum volume resting in the bucket during the bar |
depth.side(i) |
+1 bids, -1 asks, 0 bucket was on both sides |
depth.at(price) |
Volume in the bucket at that price |
depth.grid_step |
Bucket size in price units |
depth.best_bid, depth.best_ask |
Best prices at bar close |
depth.max_cell |
Volume of the heaviest bucket |
depth.bid_sum(pct), depth.ask_sum(pct) |
Side volume within pct% from mid |
depth.imbalance(pct) |
Skew in the same band: from -1 to +1 |
depth.max_bid(pct), depth.max_ask(pct) |
Price and volume of the heaviest bucket on each side within the band |
Limitations:
- Data is available where the server-side heatmap is, and on timeframes from one second.
- The server stores a band of about ±4% from price. Outside it
depth.atreturnsna; inside the band an empty bucket is0. - History depth is limited: the oldest bars of a long history return
na. - Values for the forming bar change as data arrives.
indicator("Recorded order book skew", overlay = false)
band = input.float(1.0, "Band, % of price", minval = 0.1, maxval = 4.0, step = 0.1)
imb = depth.imbalance(band)
plot(imb, "Skew", style = plot.style_columns, color = imb >= 0 ? color.teal : color.red)To draw the recorded order book under candles, one line heatmap.depth() in a script with overlay = true is enough. Colors and field thresholds are on the Inputs and drawing page.
indicator("Recorded order book as heat", overlay = true)
heatmap.config(palette = heatmap.inferno, low = 25)
heatmap.depth()Live order book and time and sales: book.* and tape.*
Live order book and time and sales are not recorded, so values exist only on the last bar and are always na on history. They require an open order book panel for the same instrument: otherwise book.available and tape.available are false and the legend shows “no open order book”. In replay mode these data are unavailable.
| Name | Returns |
|---|---|
book.bid(i), book.ask(i) |
Price of level i; 0 is best price |
book.bid_qty(i), book.ask_qty(i) |
Volume at the level, in coins |
book.spread |
Best ask minus best bid |
book.mid |
Midpoint between best prices |
book.imbalance(n) |
Volume skew across the top n levels of each side: from -1 to +1 |
book.available |
Whether an order book snapshot is available |
tape.last_price, tape.last_qty |
Price and volume of the last trade |
tape.last_side |
+1 aggressive buyer, -1 aggressive seller, 0 no trades yet |
tape.count(ms) |
Number of trades in the last ms milliseconds |
tape.buy_volume(ms), tape.sell_volume(ms) |
Aggressive buy and sell volume over the window |
tape.max_print(ms) |
Largest trade in the window |
tape.available |
Whether the time and sales feed is connected |
The time and sales window and book.imbalance depth are set as numbers in code or via an input.int setting. The window counts from the last trade time on the exchange, not from the computer clock. The script only sees the tail of the time and sales — about the last two thousand trades, so on an active market a long window won’t cover the whole interval.
When such a value is used in plot, the editor warns: “Data is available only in real time — the series is empty on history”. This is expected: to the left of the last bar the series is empty. Strategies cannot read these data — see Strategies and tester.
indicator("Order book skew", overlay = false)
levels = input.int(20, "Levels per side", minval = 1, maxval = 200)
imb = book.available ? book.imbalance(levels) : na
plot(imb, "Skew", style = plot.style_area, color = imb >= 0 ? color.teal : color.red)
hline(0, "Zero", color = color.gray)Screener: screener.*
screener metrics for the chart’s instrument — the same numbers as in its columns. There is no history: a value exists only on the last bar. The window is a string, and not every metric has every window.
| Function | Shows | Windows |
|---|---|---|
screener.change(w) |
Price change | "1m" "5m" "15m" "1h" "24h" |
screener.vol(w) |
Turnover | "1m" "5m" "15m" "1h" "24h" |
screener.avg_vol(w) |
Average turnover | "1m" "5m" "15m" "1h" |
screener.rvol(w) |
Relative volume | "1h" "24h" |
screener.natr(w) |
NATR | "1m" "5m" |
screener.oi_delta(w) |
Open interest change | "1m" "5m" "1h" "24h" |
screener.btc_corr(w) |
Correlation with BTC | "1m" "5m" |
screener.available |
Whether the instrument is covered by the screener | — |
A window not listed in the table returns na. A metric not included in your screener subscription also returns na. The editor marks output of these values with the same warning as for the order book: the series is empty on history.
indicator("Screener: RVOL and NATR", overlay = false)
plot(screener.rvol("1h"), "RVOL 1h", color = color.orange)
plot(screener.natr("5m"), "NATR 5m", color = color.teal)Exchange coverage
| Exchange | Clusters and delta | Open interest and funding |
|---|---|---|
| Binance, Bybit, OKX, Bitget, Gate.io, MEXC, KuCoin, BingX, AsterDEX | Spot and futures | Futures |
| Hyperliquid | Futures; no spot | Futures |
| Binance Alpha | No: exchange doesn’t publish aggressor side | No |
| Upbit | No | No |
| Data | Timeframes |
|---|---|
| Clusters | 30 seconds to 1 day |
| Delta | 30 seconds and up |
| Open interest, funding | 1 minute and up |
| Recorded order book | 1 second and up, where server-side heatmap is available |
| Live order book, time and sales | Any, with an open order book panel |
| Screener | Any, for instruments the screener sees |
Coverage is determined by the data server and may change. The most reliable approach is to rely on the .available flag and the legend caption.
Another timeframe and another instrument
request.security evaluates an expression on another series — a higher timeframe or another coin — and maps the result to the chart’s bars.
request.security(symbol, timeframe, expression, gaps = false, lookahead = false, ignore_invalid_symbol = false)- symbol — in “EXCHANGE:TICKER” format, as in TradingView:
"BINANCE:BTCUSDT.P", where.Pis a perpetual futures contract. The chart’s instrument issym.ticker. - timeframe — a string like
"5","60","4h","1D","1W". The chart’s timeframe istf.period. - expression — one number or multiple numbers in square brackets:
[high, low].
No lookahead. By default, a chart bar sees the value of the last closed bar of the higher timeframe. That’s why an hourly EMA on 5-minute bars is a step that only moves when the hour closes. This makes the backtest match what you would have seen live.
gaps = true— value appears only on the first chart bar where a new higher-timeframe bar appeared, thenna.ignore_invalid_symbol = true— an unknown symbol returnsna. Without this, the script stops with the error “Symbol … unknown”.
Inside the expression you can use the other series’ candles and calculations on them. You cannot pass a chart variable there — only values from input.* settings. The order book, time and sales, and screener don’t compile inside the request, and clusters, delta, open interest, and funding for another series return na. A script can have up to 8 different “instrument + timeframe” pairs and up to 32 request.security calls.
indicator("Higher-timeframe EMA", overlay = true)
len = input.int(20, "EMA length", minval = 2, maxval = 500)
tfHigh = input.timeframe("60", "Higher timeframe")
emaHigh = request.security(sym.ticker, tfHigh, ta.ema(close, len))
plot(emaHigh, "EMA", color = color.orange, style = plot.style_stepline)indicator("Spread to another exchange", overlay = false)
other = input.symbol("BYBIT:BTCUSDT.P", "Compare to")
px = request.security(other, tf.period, close, ignore_invalid_symbol = true)
plot(close - px, "Difference", color = color.orange)