Live market data (orderflow)
Read-only orderbook, trade-tape and ticker primitives streamed DIRECT from the exchange in your browser — best bid/ask, depth, imbalance, walls, live CVD, buy/sell pressure. No server, no socket in script-space, follows the chart symbol by default.
The live market-data primitives let a script READ the current orderbook, the trade tape, and the 24h ticker for the charted instrument — the same raw feeds the native Orderbook / DOM / Walls widgets use. They power orderflow indicators: depth ladders, imbalance gauges, liquidity walls, live CVD, absorption / aggression read-outs.
These are the only DSL functions that touch the network — and they do it WITHOUT breaking the sandbox (see How it stays safe). Your script never opens a socket; it just reads scalars the host already streamed.
At a glance
| Group | Functions |
|---|---|
| Top of book | bestBid bestAsk midPrice spread |
| Depth by level | bidPrice askPrice bidSize askSize |
| Aggregated depth | sumBidQty sumAskQty bookImbalance wallBid wallAsk |
| Trade tape | buyVolume sellVolume cvdLive tradeCount lastTradePrice lastTradeQty lastTradeSide |
| Ticker | tickerLast tickerBid tickerAsk tickerVol |
Calling convention
Every function takes all-optional arguments and returns a single scalar number (or NaN):
fn([level_or_n], [exchange], [symbol])
- Numeric arg — a book
level(0 = best, deeper = further from mid) or a depthn, depending on the function. - String arg(s) — the
exchange, and optionally asymbol. A trailing string that isn't a known exchange is treated as the symbol. - Omit the symbol → follows the CHART symbol. Pass a symbol to read a different instrument; pass an exchange to read a different venue (e.g. compare two exchanges in one script).
Supported exchanges:
| String | Venue |
|---|---|
"binancef" | Binance USD-M futures (default in the samples) |
"binance" | Binance spot |
"bybit" / "bybitf" | Bybit spot / linear futures |
"coinbase" | Coinbase Advanced Trade |
"hyperliquid" | Hyperliquid perps |
bb = bestBid() // chart symbol, default exchange
bb2 = bestBid("bybit") // chart symbol on Bybit
bb3 = bestBid("binancef", "ETHUSDT") // a different symbol
sz = bidSize(3, "coinbase") // size at the 4th bid level on Coinbase
Live-only & cold reads
These read a live stream, not historical bars. Two rules:
NaNuntil warm. A feed takes a moment to connect and fill. Every function returnsNaNwhile cold, off a live chart, or on an unknown exchange. Always guard withisna(...)and draw a "waiting…" hint on the cold path.- Trade & ticker totals are cumulative SINCE subscribe — i.e. since the indicator was added or the socket last reconnected, NOT since session open.
buyVolume/sellVolume/cvdLive/tradeCountreset to 0 on reconnect. They are a running tally, not a per-bar value.
bb = bestBid()
if isna(bb)
plotLabel(last_bar_time, at(close, last_bar_index), "Waiting for orderbook…",
color="#FFFFFF", bg="rgba(246,70,93,0.72)", anchor="left", size=12)
else
// … safe to read the book here …
end
Top of book
| Function | Returns |
|---|---|
bestBid(exchange?, symbol?) | highest bid price |
bestAsk(exchange?, symbol?) | lowest ask price |
midPrice(exchange?, symbol?) | (bestBid + bestAsk) / 2 |
spread(exchange?, symbol?) | bestAsk − bestBid (absolute price) |
Depth by level
| Function | Returns |
|---|---|
bidPrice(level=0, exchange?, symbol?) | price at bid level |
askPrice(level=0, exchange?, symbol?) | price at ask level |
bidSize(level=0, exchange?, symbol?) | resting size at bid level |
askSize(level=0, exchange?, symbol?) | resting size at ask level |
level is clamped to the available depth (up to 50 levels per side where the venue provides them). Level 0 is the inside market; higher levels step away from mid.
// Walk the top 10 bid levels and sum the size below mid.
total = arrFrom(0) // box: a plain acc inside a for is a LOCAL
for i = 0 to 9
s = bidSize(i)
if not isna(s)
arrSet(total, 0, arrGet(total, 0) + s)
end
end
Reassigning a plain scalar inside a
fordeclares a LOCAL binding that never escapes the loop. Use thearrFrom/arrSetbox pattern (or the aggregated-depth helpers below) instead. See Pitfalls.
Aggregated depth
| Function | Returns |
|---|---|
sumBidQty(n=10, exchange?, symbol?) | total bid size over the top n levels |
sumAskQty(n=10, exchange?, symbol?) | total ask size over the top n levels |
bookImbalance(n=10, exchange?, symbol?) | (sumBid − sumAsk) / (sumBid + sumAsk) over top n — ranges −1 (all ask) to +1 (all bid) |
wallBid(n=20, exchange?, symbol?) | largest single resting bid SIZE within the top n |
wallAsk(n=20, exchange?, symbol?) | largest single resting ask SIZE within the top n |
wallBid / wallAsk return the wall size, not its price. To get the price of the biggest wall, scan bidSize / bidPrice yourself (see the Liquidity Walls recipe).
Trade tape
| Function | Returns |
|---|---|
buyVolume(exchange?, symbol?) | cumulative aggressive BUY volume since subscribe |
sellVolume(exchange?, symbol?) | cumulative aggressive SELL volume since subscribe |
cvdLive(exchange?, symbol?) | live CVD = buyVolume − sellVolume |
tradeCount(exchange?, symbol?) | number of prints since subscribe |
lastTradePrice(exchange?, symbol?) | price of the most recent print |
lastTradeQty(exchange?, symbol?) | size of the most recent print |
lastTradeSide(exchange?, symbol?) | aggressor side: +1 buy, −1 sell |
Ticker
| Function | Returns |
|---|---|
tickerLast(exchange?, symbol?) | last price (24h ticker stream) |
tickerBid(exchange?, symbol?) | best bid (ticker) |
tickerAsk(exchange?, symbol?) | best ask (ticker) |
tickerVol(exchange?, symbol?) | 24h rolling volume |
Examples
These three ship as built-in samples (Script editor → Samples). They all park a big panel to the RIGHT of the last candle so they never crowd live price.
Example — Book Pressure (imbalance gauge)
@version 1
@name "Book Pressure"
@input depth = input.int(20, "Depth (levels)", minval=1, maxval=50)
@input rightOff = input.int(36, "Right offset (bars)", minval=0, maxval=200)
@input widthBars = input.int(44, "Bar width (bars)", minval=4, maxval=400)
@input heightPct = input.float(3.5, "Bar height (% price)", minval=0.1, maxval=50, step=0.1)
tNow = last_bar_time
sb = sumBidQty(depth)
sa = sumAskQty(depth)
if isna(sb) or isna(sa) or (sb + sa) <= 0
plotLabel(tNow, at(close, last_bar_index), "Waiting for orderbook…",
color="#FFFFFF", bg="rgba(246,70,93,0.72)", anchor="left", size=12)
else
barMs = tNow - at(time, last_bar_index - 1)
if barMs <= 0
barMs = 60000
end
tA = tNow + barMs * rightOff
tB = tA + barMs * widthBars
c = midPrice()
half = c * heightPct * 0.005
tot = sb + sa
tSplit = tA + (tB - tA) * (sb / tot)
plotBox(tA, c - half, tSplit, c + half, bg=withAlpha("#0ECB81", 0.80), border=withAlpha("#0ECB81", 0.95))
plotBox(tSplit, c - half, tB, c + half, bg=withAlpha("#F6465D", 0.80), border=withAlpha("#F6465D", 0.95))
imb = (sb - sa) / tot
col = iff(imb >= 0, "#0ECB81", "#F6465D")
plotLabel((tA + tB) / 2, c + half, "IMBALANCE " + tostring(imb * 100, "0.0") + "%",
color="#FFFFFF", bg=withAlpha(col, 0.90), anchor="above", size=13)
end
Example — Live CVD & Tape
@version 1
@name "Live CVD"
tNow = last_bar_time
bv = buyVolume()
sv = sellVolume()
if isna(bv) and isna(sv)
plotLabel(tNow, at(close, last_bar_index), "Waiting for tape…",
color="#FFFFFF", bg="rgba(246,70,93,0.72)", anchor="left", size=12)
else
cvd = cvdLive()
yp = at(close, last_bar_index)
col = iff(cvd >= 0, "#0ECB81", "#F6465D")
plotLabel(tNow, yp,
"CVD " + tostring(cvd, "#.##") + " B " + tostring(bv, "#.##") + " S " + tostring(sv, "#.##"),
color="#FFFFFF", bg=withAlpha(col, 0.85), anchor="left", size=12)
end
Example — Liquidity Walls
@version 1
@name "Liquidity Walls"
@input depth = input.int(20, "Scan depth", minval=2, maxval=50)
@input minMult = input.float(2.5, "Wall vs avg (x)", minval=1, maxval=20, step=0.5)
tNow = last_bar_time
bb = bestBid()
if not isna(bb)
barMs = tNow - at(time, last_bar_index - 1)
if barMs <= 0
barMs = 60000
end
bAvg = sumBidQty(depth) / depth
// Biggest bid wall + its PRICE (box writeback — no scalar-in-loop).
bBox = arrFrom(0, 0) // [size, price]
for i = 0 to depth - 1
s = bidSize(i)
p = bidPrice(i)
if not isna(s) and not isna(p) and s > arrGet(bBox, 0)
arrSet(bBox, 0, s)
arrSet(bBox, 1, p)
end
end
bMax = arrGet(bBox, 0)
if bMax > 0 and bMax >= bAvg * minMult
bpx = arrGet(bBox, 1)
plotHline(bpx, color="#0ECB81", width=2)
plotLabel(tNow, bpx, tostring(bMax, "#.##"), color="#0ECB81", anchor="left", size=12)
end
end
How it stays safe
The DSL sandbox forbids fetch / WebSocket / any I/O from script-space — that invariant is unchanged. These primitives don't break it:
- The host (not your script) owns the WebSocket. Your script calls
bidPrice()etc.; the host lazily opens the public exchange socket on first request and feeds the read-only snapshot back. The script only ever READS a scalar. - No credentials. The exchanges' public market streams need no auth. Nothing about your account is exposed.
- No alerts / no BE. Live market reads run in the browser only. A script that uses them can't drive server-side alerts off the live feed (there's nothing for the BE to replay) — it's a live, on-chart overlay. Save still works; the live reads just return
NaNin the BE preview.
Performance & limits
- Main-thread only. A script that mentions any market function is pinned to the main thread (the worker realm has no feed). This is automatic — no directive needed.
- Connection budget. The host caps the number of live streams and de-duplicates them: two indicators reading the same symbol/venue share one socket. Idle streams are dropped; hidden tabs park the feed.
- Keep compute light. These run every tick. Reading dozens of levels + drawing per-level labels can blow the 16 ms per-eval budget (the engine then auto-throttles to bar-close). Prefer the aggregated helpers (
sumBidQty,bookImbalance) over hand loops, and turn value labels off by default. - Lifecycle. A feed lives only while its indicator is on the chart — alive on add, gone on remove. Cumulative tape totals reset on reconnect.