
QuaScript Documentation
Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1
book
Bid / Ask Depth Bands
analysis · presets/analysis/book/bid_ask_depth_bands.qs
The prices where the book's cumulative size reaches 25 %, 50 % and 75 % of each side's visible depth. Three nested bands per side show how the liquidity is distributed: tight inner bands mean size sits near the touch, wide ones that it is stacked deep. Needs the order book (L2).
analysis "Bid / Ask Depth Bands" {
overlay = true
render = webgpu
short_title = "BADB"
}
/// The prices where the book's cumulative size reaches 25 %, 50 % and 75 % of each side's visible
/// depth. Three nested bands per side show how the liquidity is distributed: tight inner bands mean
/// size sits near the touch, wide ones that it is stacked deep. Needs the order book (L2).
bids = book.bids
asks = book.asks
nb = grid.rows(bids)
na_ = grid.rows(asks)
btot = nb > 0 ? grid.get(bids, nb - 1, book.col_total) : na
atot = na_ > 0 ? grid.get(asks, na_ - 1, book.col_total) : na
b25 = na
b50 = na
b75 = na
for i in 0..nb {
c = grid.get(bids, i, book.col_total)
p = grid.get(bids, i, book.col_price)
if math.is_na(b25) and c >= 0.25 * btot { b25 := p }
if math.is_na(b50) and c >= 0.5 * btot { b50 := p }
if math.is_na(b75) and c >= 0.75 * btot { b75 := p }
}
a25 = na
a50 = na
a75 = na
for i in 0..na_ {
c = grid.get(asks, i, book.col_total)
p = grid.get(asks, i, book.col_price)
if math.is_na(a25) and c >= 0.25 * atot { a25 := p }
if math.is_na(a50) and c >= 0.5 * atot { a50 := p }
if math.is_na(a75) and c >= 0.75 * atot { a75 := p }
}
pa75 = render.line(a75, "Ask 75 %", color = color.translucent(color.rose.300, 50), width = 1)
pa50 = render.line(a50, "Ask 50 %", color = color.translucent(color.rose.300, 30), width = 1)
pa25 = render.line(a25, "Ask 25 %", color = color.rose.300, width = 1.2)
pb25 = render.line(b25, "Bid 25 %", color = color.emerald.300, width = 1.2)
pb50 = render.line(b50, "Bid 50 %", color = color.translucent(color.emerald.300, 30), width = 1)
pb75 = render.line(b75, "Bid 75 %", color = color.translucent(color.emerald.300, 50), width = 1)
render.band(pa75, pa25, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.rose.500, opacity = 0.25, title = "Ask depth")
render.band(pb25, pb75, texture = "gradient", direction = "edges", upper_color = color.emerald.500, lower_color = color.slate.900, opacity = 0.25, title = "Bid depth")Book Absorption Zones
analysis · presets/analysis/book/book_absorption_zones.qs · uses viz/premium
Absorption seen from both sides. From the bars: heavy relative volume, a range small against ATR, and a close away from the extreme the volume pushed into. From the book: the wall at that extreme was still there at the close (its size did not fall), so the volume was absorbed by resting orders rather than pushing them away. Each confirmed absorption becomes a zone at the wall's price that extends right until a close goes through it. Bars without a recorded book can still qualify on OHLCV alone, drawn fainter. Needs the order book (L2) for the confirmation.
analysis "Book Absorption Zones" {
overlay = true
render = webgpu
short_title = "BAZ"
max_objects = 60
}
/// Absorption seen from both sides. From the bars: heavy relative volume, a range small against
/// ATR, and a close away from the extreme the volume pushed into. From the book: the wall at that
/// extreme was still there at the close (its size did not fall), so the volume was absorbed by
/// resting orders rather than pushing them away. Each confirmed absorption becomes a zone at the
/// wall's price that extends right until a close goes through it.
/// Bars without a recorded book can still qualify on OHLCV alone, drawn fainter.
/// Needs the order book (L2) for the confirmation.
use "viz/premium" as pr
rv_len = param.int(50, title = "Relative volume window", min = 5, max = 500)
rv_min = param.float(1.8, title = "Heavy volume (x average)", min = 1, max = 10, step = 0.1)
rng = param.float(0.8, title = "Small range (x ATR)", min = 0.1, max = 2, step = 0.05)
a = ta.atr(14)
rv = volume / math.max(ta.sma(volume, rv_len), 1e-12)
clv = ((close - low) - (high - close)) / math.max(high - low, 1e-12)
(bwp, bws) = book.wall(book.bid_side, 10)
(awp, aws) = book.wall(book.ask_side, 10)
heavy = rv >= rv_min and high - low <= rng * a
sell_abs = heavy and clv > 0.25 // sellers hit the lows, close rebounds
buy_abs = heavy and clv < -0.25 // buyers hit the highs, close falls back
bid_held = not math.is_na(bws) and not math.is_na(bws[1]) and bws >= bws[1] * 0.8
ask_held = not math.is_na(aws) and not math.is_na(aws[1]) and aws >= aws[1] * 0.8
conf_bid = sell_abs and bid_held
conf_ask = buy_abs and ask_held
render.mark(conf_bid, shape = shape.diamond, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Bids absorbed selling")
render.mark(conf_ask, shape = shape.diamond, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Asks absorbed buying")
render.mark(sell_abs and not conf_bid, shape = shape.circle, at = anchor.below_bar, color = color.translucent(color.emerald.300, 50), size = 0.5, title = "Bar absorption (no book)")
render.mark(buy_abs and not conf_ask, shape = shape.circle, at = anchor.above_bar, color = color.translucent(color.rose.300, 50), size = 0.5, title = "Bar absorption (no book)")
alert.rule(conf_bid or conf_ask, "Book absorption", "Heavy volume absorbed by a wall that held")
struct Zone { z: id = na, px: float = na, side: int = 1 }
persist zones = list.create(Zone)
if conf_bid or sell_abs {
px = conf_bid ? math.min(bwp, low) : low
list.push(zones, Zone(z = draw.zone.create(bar.index, px + 0.15 * a, bar.index + 1, px - 0.15 * a, color = color.translucent(color.emerald.500, conf_bid ? 78 : 90), border_color = color.translucent(color.emerald.400, conf_bid ? 20 : 60)), px = px, side = 1))
}
if conf_ask or buy_abs {
px = conf_ask ? math.max(awp, high) : high
list.push(zones, Zone(z = draw.zone.create(bar.index, px + 0.15 * a, bar.index + 1, px - 0.15 * a, color = color.translucent(color.rose.500, conf_ask ? 78 : 90), border_color = color.translucent(color.rose.400, conf_ask ? 20 : 60)), px = px, side = -1))
}
i = 0
while i < list.size(zones) {
zz = list.get(zones, i)
broken = zz.side == 1 ? close < zz.px - 0.15 * a : close > zz.px + 0.15 * a
if broken {
list.remove(zones, i)
} else {
draw.zone.update(zz.z, right = bar.index + 1)
i += 1
}
}
while list.size(zones) > 25 { list.shift(zones) }Book Gravity Bands
analysis · presets/analysis/book/book_gravity_bands.qs
The book's centre of gravity. The size-weighted price of the top bid levels and of the top ask levels form a band around the price; their size-weighted average is where the resting liquidity balances. A close above the centre of gravity with the band tilted up means the book supports the move; price far from the centre tends to be pulled back to it. Bands are smoothed and drawn as a GPU gradient; on bars without a book the last centre carries. Needs the order book (L2).
analysis "Book Gravity Bands" {
overlay = true
render = webgpu
short_title = "BGB"
}
/// The book's centre of gravity. The size-weighted price of the top bid levels and of the top ask
/// levels form a band around the price; their size-weighted average is where the resting
/// liquidity balances. A close above the centre of gravity with the band tilted up means the book
/// supports the move; price far from the centre tends to be pulled back to it.
/// Bands are smoothed and drawn as a GPU gradient; on bars without a book the last centre carries.
/// Needs the order book (L2).
levels = param.int(10, title = "Levels per side", min = 1, max = 200)
smooth = param.int(3, title = "Smoothing", min = 1, max = 50)
bv = book.vwap(book.bid_side, levels)
av = book.vwap(book.ask_side, levels)
bq = book.bid_volume(levels)
aq = book.ask_volume(levels)
g_raw = (bv * bq + av * aq) / math.max(bq + aq, 1e-12)
persist g_last = na
g_last := math.is_na(g_raw) ? g_last : g_raw
g = ta.ema(g_last, smooth)
pa = render.line(ta.ema(av, smooth), "Ask gravity", color = color.translucent(color.rose.300, 40), width = 1)
pg = render.line(g, "Centre of gravity", color = close > g ? color.emerald.300 : color.rose.300, width = 2)
pb = render.line(ta.ema(bv, smooth), "Bid gravity", color = color.translucent(color.emerald.300, 40), width = 1)
render.band(pa, pg, texture = "gradient", direction = "edges", upper_color = color.rose.500, lower_color = color.slate.900, opacity = 0.22, title = "Ask side")
render.band(pg, pb, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.emerald.500, opacity = 0.22, title = "Bid side")
render.mark(ta.cross_up(close, g), shape = shape.circle, at = anchor.below_bar, color = color.emerald.300, size = 0.5, title = "Above gravity")
render.mark(ta.cross_down(close, g), shape = shape.circle, at = anchor.above_bar, color = color.rose.300, size = 0.5, title = "Below gravity")
alert.rule(ta.cross(close, g), "Gravity cross", "Price crossed the order book's centre of gravity")Book Imbalance Spectrum
analysis · presets/analysis/book/book_imbalance_spectrum.qs · uses viz/premium
How the resting order book leans, measured at every depth at once, next to what the bars did. The lower rows are the size imbalance of the top 1, 2, 3, 5, 8, 13, 21 and 34 levels and of the whole book; the upper rows the distance-weighted pressure with decays from 0.05 % to 1 %, so the touch and the deep book read separately. Teal = bids outweigh asks, rose = asks outweigh bids. The line fuses the book with the bar: the average book lean plus the bar's own close-location delta (its buying / selling from OHLCV), weighted by relative volume. On bars without a recorded book the line falls back to the bar delta alone, so history stays meaningful. Divergences: price makes a new swing that the fused lean does not confirm. Needs the order book (L2; works on L1 with the top row only).
analysis "Book Imbalance Spectrum" {
pane = hpane
height = 0.28
render = webgpu
}
/// How the resting order book leans, measured at every depth at once, next to what the bars did.
/// The lower rows are the size imbalance of the top 1, 2, 3, 5, 8, 13, 21 and 34 levels and of the
/// whole book; the upper rows the distance-weighted pressure with decays from 0.05 % to 1 %, so the
/// touch and the deep book read separately. Teal = bids outweigh asks, rose = asks outweigh bids.
/// The line fuses the book with the bar: the average book lean plus the bar's own close-location
/// delta (its buying / selling from OHLCV), weighted by relative volume. On bars without a recorded
/// book the line falls back to the bar delta alone, so history stays meaningful.
/// Divergences: price makes a new swing that the fused lean does not confirm.
/// Needs the order book (L2; works on L1 with the top row only).
use "viz/premium" as pr
rv_len = param.int(50, title = "Relative volume window", min = 5, max = 500, group = "Fusion")
w_bar = param.float(0.4, title = "Bar delta weight", min = 0, max = 1, step = 0.05, group = "Fusion")
smooth = param.int(3, title = "Smoothing", min = 1, max = 50, group = "Fusion")
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
lean = 0.0
rows = 0
k = 0
for n in [1, 2, 3, 5, 8, 13, 21, 34, 0] {
imb = book.imbalance(n)
if not math.is_na(imb) {
lean := lean + imb
rows := rows + 1
}
top = render.line(k + 0.5, str.format("Imbalance edge {}", n), color = color.transparent, width = 0)
render.band(top, edge, color = pr.diverging(imb, 0.6, color.teal.300, color.rose.400), opacity = 1, title = n == 0 ? "Whole book" : str.format("Top {} levels", n))
edge := top
k += 1
}
for d in [0.05, 0.1, 0.25, 0.5, 1.0] {
pres = book.pressure(d)
top = render.line(k + 0.5, str.format("Pressure edge {}", d), color = color.transparent, width = 0)
render.band(top, edge, color = pr.diverging(pres, 0.6, color.cyan.300, color.pink.400), opacity = 1, title = str.format("Pressure {} %", d))
edge := top
k += 1
}
clv = ((close - low) - (high - close)) / math.max(high - low, 1e-12)
rv = math.min(volume / math.max(ta.sma(volume, rv_len), 1e-12), 3)
book_lean = rows > 0 ? lean / rows : na
fused_raw = math.is_na(book_lean) ? clv * rv / 3 : (1 - w_bar) * book_lean + w_bar * clv * rv / 3
fused = ta.ema(fused_raw, smooth)
render.line(6.5 + math.clamp(fused, -1, 1) * 6.5, "Fused lean", color = fused >= 0 ? color.teal.200 : color.rose.300, width = 2)
render.line(6.5, "Neutral", color = color.translucent(color.slate.400, 60), width = 0.6)
(bull_div, bear_div) = ta.divergence(fused, 5, 5, 80, false)
render.mark(bull_div, shape = shape.triangle_up, at = anchor.bottom, color = color.teal.300, title = "Bullish book divergence")
render.mark(bear_div, shape = shape.triangle_down, at = anchor.top, color = color.rose.400, title = "Bearish book divergence")
alert.rule(bull_div or bear_div, "Book divergence", "The fused book / bar lean diverges from price at a pivot")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Book Imbalance Spectrum", "Order book × OHLCV")
pr.row(t, 2, "Book depth", book.depth == 3 ? "L3 orders" : book.depth == 2 ? str.format("L2 · {} levels", book.levels) : book.depth == 1 ? "L1 top of book" : "No book", book.depth > 0 ? color.white : pr.WARN)
pr.row(t, 3, "Whole-book imbalance", math.is_na(book.imbalance()) ? "-" : str.format("{:+.1%}", book.imbalance()), book.imbalance() >= 0 ? pr.BULL : pr.BEAR)
pr.row(t, 4, "Fused lean", str.format("{:+.2f}", fused), fused >= 0 ? pr.BULL : pr.BEAR)
}Book Pressure Candles
analysis · presets/analysis/book/book_pressure_candles.qs
Candles coloured by whether the bar and the book agree. The bar's own pressure comes from OHLCV (where it closed in its range, weighted by relative volume); the book's from the distance-weighted imbalance of the resting orders at the bar's close. Both bullish = bright teal, both bearish = bright rose, a bar that rallied into a book leaning to the asks (distribution) = amber, a bar that fell onto supportive bids (accumulation) = violet. Marks flag the first bar of each disagreement, often where a move runs into resting liquidity. Bars without a recorded book keep their normal colours.
analysis "Book Pressure Candles" {
overlay = true
short_title = "BPC"
}
/// Candles coloured by whether the bar and the book agree. The bar's own pressure comes from
/// OHLCV (where it closed in its range, weighted by relative volume); the book's from the
/// distance-weighted imbalance of the resting orders at the bar's close. Both bullish = bright
/// teal, both bearish = bright rose, a bar that rallied into a book leaning to the asks
/// (distribution) = amber, a bar that fell onto supportive bids (accumulation) = violet.
/// Marks flag the first bar of each disagreement, often where a move runs into resting liquidity.
/// Bars without a recorded book keep their normal colours.
decay = param.float(0.25, title = "Book decay (%)", min = 0.02, max = 2, step = 0.01, group = "Book")
rv_len = param.int(30, title = "Relative volume window", min = 5, max = 500, group = "Bar")
thr = param.float(0.15, title = "Agreement threshold", min = 0.01, max = 0.8, step = 0.01, group = "Bar")
clv = ((close - low) - (high - close)) / math.max(high - low, 1e-12)
rv = math.min(volume / math.max(ta.sma(volume, rv_len), 1e-12), 2)
bar_p = clv * rv / 2
bk = book.pressure(decay)
bull = bar_p > thr and bk > thr
bear = bar_p < -thr and bk < -thr
dist = bar_p > thr and bk < -thr
accum = bar_p < -thr and bk > thr
c = math.is_na(bk) ? na : bull ? color.teal.400 : bear ? color.rose.500 : dist ? color.amber.400 : accum ? color.violet.400 : color.slate.500
render.candle_color(c)
render.mark(dist and not dist[1], shape = shape.circle, at = anchor.above_bar, color = color.amber.300, size = 0.5, title = "Distribution")
render.mark(accum and not accum[1], shape = shape.circle, at = anchor.below_bar, color = color.violet.300, size = 0.5, title = "Accumulation")
alert.rule(dist and not dist[1], "Distribution", "Price rose into a book leaning to the asks")
alert.rule(accum and not accum[1], "Accumulation", "Price fell onto a book leaning to the bids")Book RSI
analysis · presets/analysis/book/book_rsi.qs
An RSI computed from the order book instead of price: book-side gains are increases in resting bid size near the mid, losses increases in resting ask size. It oscillates 0 .. 100 like RSI; extremes mean one side of the book has been building persistently. Price RSI is drawn faint for comparison. Needs the order book (L2).
analysis "Book RSI" {
pane = hpane
height = 0.2
render = webgpu
}
/// An RSI computed from the order book instead of price: book-side gains are increases in resting
/// bid size near the mid, losses increases in resting ask size. It oscillates 0 .. 100 like RSI;
/// extremes mean one side of the book has been building persistently. Price RSI is drawn faint for
/// comparison. Needs the order book (L2).
pct = param.float(0.25, title = "Band around mid (%)", min = 0.02, max = 5, step = 0.01)
len = param.int(14, title = "Length", min = 2, max = 200)
b = book.liquidity(pct, book.bid_side)
a = book.liquidity(pct, book.ask_side)
d = math.coalesce((b - b[1]) - (a - a[1]), 0)
up = ta.rma(math.max(d, 0), len)
dn = ta.rma(math.max(-d, 0), len)
brsi = 100 - 100 / (1 + up / math.max(dn, 1e-12))
render.line(brsi, "Book RSI", color = brsi > 50 ? color.teal.300 : color.rose.300, width = 2)
render.line(ta.rsi(close, len), "Price RSI", color = color.translucent(color.slate.300, 50), width = 1)
render.level(70, "70", color = color.translucent(color.teal.500, 60))
render.level(30, "30", color = color.translucent(color.rose.500, 60))
(bd, sd) = ta.divergence(brsi, 5, 5, 60, false)
render.mark(bd, shape = shape.triangle_up, at = anchor.bottom, color = color.teal.300, title = "Bullish book divergence")
render.mark(sd, shape = shape.triangle_down, at = anchor.top, color = color.rose.300, title = "Bearish book divergence")Book Steepness
analysis · presets/analysis/book/book_steepness.qs
How fast resting size builds with distance from the mid, per side. Steepness is the log ratio of the size within 1 % to the size within 0.1 %: a high value means the touch is thin and size sits further away (price can slip until it reaches it); a low value means size is stacked at the touch. The difference between the sides tells which way the near book is weaker. Needs the order book (L2).
analysis "Book Steepness" {
pane = hpane
height = 0.2
render = webgpu
}
/// How fast resting size builds with distance from the mid, per side. Steepness is the log ratio of
/// the size within 1 % to the size within 0.1 %: a high value means the touch is thin and size sits
/// further away (price can slip until it reaches it); a low value means size is stacked at the
/// touch. The difference between the sides tells which way the near book is weaker.
/// Needs the order book (L2).
near = param.float(0.1, title = "Near band (%)", min = 0.01, max = 2, step = 0.01)
far = param.float(1.0, title = "Far band (%)", min = 0.05, max = 10, step = 0.05)
bid_st = math.log(math.max(book.liquidity(far, book.bid_side), 1e-12) / math.max(book.liquidity(near, book.bid_side), 1e-12))
ask_st = math.log(math.max(book.liquidity(far, book.ask_side), 1e-12) / math.max(book.liquidity(near, book.ask_side), 1e-12))
pb = render.line(bid_st, "Bid steepness", color = color.emerald.300, width = 1.6)
pa = render.line(ask_st, "Ask steepness", color = color.rose.300, width = 1.6)
render.band(pb, pa, color = bid_st > ask_st ? color.rose.500 : color.emerald.500, opacity = 0.2, title = "Weaker side")
diff = ta.ema(ask_st - bid_st, 5)
render.mark(ta.cross_up(diff, 0), shape = shape.triangle_up, at = anchor.bottom, color = color.emerald.300, title = "Asks thinner near the touch")
render.mark(ta.cross_down(diff, 0), shape = shape.triangle_down, at = anchor.top, color = color.rose.300, title = "Bids thinner near the touch")Book / Volume Ratio
analysis · presets/analysis/book/book_volume_ratio.qs
Resting size near the price against the size that actually traded in the bar. A high ratio is a patient market (more waiting than trading); a collapsing ratio is a market consuming its book, which usually comes with range expansion. Plotted as a log ratio with its z-score shading. Needs the order book (L2).
analysis "Book / Volume Ratio" {
pane = hpane
height = 0.2
render = webgpu
}
/// Resting size near the price against the size that actually traded in the bar. A high ratio is
/// a patient market (more waiting than trading); a collapsing ratio is a market consuming its book,
/// which usually comes with range expansion. Plotted as a log ratio with its z-score shading.
/// Needs the order book (L2).
pct = param.float(0.5, title = "Band around mid (%)", min = 0.02, max = 5, step = 0.01)
lr = math.log(math.max(book.liquidity(pct), 1e-12) / math.max(volume, 1e-12))
z = stat.zscore(lr, 200)
render.line(lr, "Log book / volume", color = color.sky.300, width = 1.8)
render.background(z < -1.5 ? color.translucent(color.orange.500, 85) : z > 1.5 ? color.translucent(color.sky.500, 90) : na)
alert.rule(z < -1.5 and z[1] >= -1.5, "Book consumed", "Traded volume is outpacing the resting book")Depth Distance Heat
analysis · presets/analysis/book/depth_distance_heat.qs · uses viz/premium
The shape of the book by distance from the mid. Each row compares the resting bid size with the resting ask size within a band of distance (0.05 % up to 2 %): teal rows = more bids than asks at that distance, rose rows = more asks. Near rows show the touch, far rows the deep book, so a book that is bid near the price but offered further away (or the reverse) is visible at once. The line is the bar's close-location value (OHLCV) for comparison. Needs the order book (L2).
analysis "Depth Distance Heat" {
pane = hpane
height = 0.26
render = webgpu
}
/// The shape of the book by distance from the mid. Each row compares the resting bid size with the
/// resting ask size within a band of distance (0.05 % up to 2 %): teal rows = more bids than asks
/// at that distance, rose rows = more asks. Near rows show the touch, far rows the deep book, so a
/// book that is bid near the price but offered further away (or the reverse) is visible at once.
/// The line is the bar's close-location value (OHLCV) for comparison.
/// Needs the order book (L2).
use "viz/premium" as pr
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
k = 0
for d in [0.05, 0.1, 0.15, 0.25, 0.35, 0.5, 0.75, 1.0, 1.5, 2.0] {
b = book.liquidity(d, book.bid_side)
a = book.liquidity(d, book.ask_side)
asym = (b + a) > 0 ? (b - a) / (b + a) : na
top = render.line(k + 0.5, str.format("Edge {} %", d), color = color.transparent, width = 0)
render.band(top, edge, color = pr.diverging(asym, 0.6, color.teal.300, color.rose.400), opacity = 1, title = str.format("Within {} %", d))
edge := top
k += 1
}
clv = ((close - low) - (high - close)) / math.max(high - low, 1e-12)
render.line(4.5 + ta.ema(clv, 3) * 4.5, "Bar close location", color = color.translucent(color.white, 20), width = 1.6)Depth-Volume Envelope
analysis · presets/analysis/book/depth_volume_envelope.qs · uses viz/premium
Where a slice of a normal bar's volume would push price if it hit the book at once. On every bar a fraction (25 % by default) of the typical bar volume (its moving average, from OHLCV) is walked through the resting asks and the resting bids: the ask price where the cumulative size reaches it is the upper edge, the bid price the lower edge. A deep book makes a narrow envelope, a thin book a wide one. Comparing the bar with its envelope separates aggression from absorption: a bar whose range is unusually large for its envelope (top 5 % of its history) on normal volume found little liquidity (a sweep); a bar with heavy volume that stays inside it was absorbed by the book. Needs the order book (L2). The envelope is na on bars without a recorded book.
analysis "Depth-Volume Envelope" {
overlay = true
render = webgpu
short_title = "DVE"
}
/// Where a slice of a normal bar's volume would push price if it hit the book at once. On every bar
/// a fraction (25 % by default) of the typical bar volume (its moving average, from OHLCV) is walked through the resting asks and the resting
/// bids: the ask price where the cumulative size reaches it is the upper edge, the bid price the
/// lower edge. A deep book makes a narrow envelope, a thin book a wide one.
/// Comparing the bar with its envelope separates aggression from absorption: a bar whose range is
/// unusually large for its envelope (top 5 % of its history) on normal volume found little
/// liquidity (a sweep); a bar with heavy volume
/// that stays inside it was absorbed by the book.
/// Needs the order book (L2). The envelope is na on bars without a recorded book.
use "viz/premium" as pr
vol_len = param.int(20, title = "Typical volume (bars)", min = 2, max = 500, group = "Envelope")
frac = param.float(0.25, title = "Volume fraction", min = 0.01, max = 5, step = 0.01, group = "Envelope")
rv_hi = param.float(1.8, title = "Heavy volume (x average)", min = 1, max = 10, step = 0.1, group = "Signals")
q = ta.sma(volume, vol_len) * frac
bids = book.bids
asks = book.asks
up_px = na
for i in 0..grid.rows(asks) {
if math.is_na(up_px) and grid.get(asks, i, book.col_total) >= q { up_px := grid.get(asks, i, book.col_price) }
}
dn_px = na
for i in 0..grid.rows(bids) {
if math.is_na(dn_px) and grid.get(bids, i, book.col_total) >= q { dn_px := grid.get(bids, i, book.col_price) }
}
// a book thinner than the volume: the far end of the visible book is the lower bound of the move
up_edge = math.is_na(up_px) and grid.rows(asks) > 0 ? grid.get(asks, grid.rows(asks) - 1, book.col_price) : up_px
dn_edge = math.is_na(dn_px) and grid.rows(bids) > 0 ? grid.get(bids, grid.rows(bids) - 1, book.col_price) : dn_px
thin = (math.is_na(up_px) and grid.rows(asks) > 0) or (math.is_na(dn_px) and grid.rows(bids) > 0)
pu = render.line(up_edge, "Upper edge", color = color.translucent(color.rose.300, 30), width = 1.2)
pm = render.line(book.mid, "Mid", color = color.translucent(color.slate.300, 60), width = 0.8)
pd = render.line(dn_edge, "Lower edge", color = color.translucent(color.emerald.300, 30), width = 1.2)
width_pct = (up_edge - dn_edge) / math.max(book.mid, 1e-12) * 100
render.band(pu, pm, texture = "gradient", direction = "edges", upper_color = color.rose.500, lower_color = color.slate.900, opacity = 0.2, title = "Ask depth")
render.band(pm, pd, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.emerald.500, opacity = 0.2, title = "Bid depth")
rv = volume / math.max(ta.sma(volume, vol_len), 1e-12)
// signals only where the book was deep enough to define the edge (not a thin book's far end)
// range per envelope width, judged against its own history (books and bars differ by market)
ratio = (high - low) / math.max(up_px[1] - dn_px[1], 1e-12)
extreme = ratio > stat.percentile(ratio, 300, 95)
sweep_up = extreme and close > up_px[1] and rv < rv_hi and close > open
sweep_dn = extreme and close < dn_px[1] and rv < rv_hi and close < open
absorbed = not math.is_na(up_px[1]) and not math.is_na(dn_px[1]) and rv >= rv_hi and high <= up_px[1] and low >= dn_px[1]
render.mark(sweep_up, shape = shape.arrow_up, at = anchor.above_bar, color = color.rose.300, size = 0.7, title = "Swept the asks")
render.mark(sweep_dn, shape = shape.arrow_down, at = anchor.below_bar, color = color.emerald.300, size = 0.7, title = "Swept the bids")
render.mark(absorbed, shape = shape.diamond, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Absorbed")
render.candle_color(absorbed ? color.amber.400 : na)
alert.rule(absorbed, "Absorption", "Heavy volume stayed inside the depth envelope")
alert.rule(sweep_up or sweep_dn, "Book sweep", "Normal volume broke through the depth envelope")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Depth-Volume Envelope", "Order book × OHLCV")
pr.row(t, 2, "Envelope width", math.is_na(width_pct) ? "no book" : str.format("{:.3f} %", width_pct), color.white)
pr.row(t, 3, "Book vs volume", thin ? "Thinner than a bar" : "Deeper than a bar", thin ? pr.WARN : pr.BULL)
}Impact Asymmetry
analysis · presets/analysis/book/impact_asymmetry_candles.qs
Which direction is cheaper to push: the cost of buying versus selling the same size through the book. When buying is much cheaper than selling (asks thin, bids deep), up-moves meet little resistance; candles are tinted accordingly and marks flag extreme asymmetry. Needs the order book (L2).
analysis "Impact Asymmetry" {
overlay = true
short_title = "IA"
}
/// Which direction is cheaper to push: the cost of buying versus selling the same size through the
/// book. When buying is much cheaper than selling (asks thin, bids deep), up-moves meet little
/// resistance; candles are tinted accordingly and marks flag extreme asymmetry.
/// Needs the order book (L2).
frac = param.float(0.05, title = "Order size (x typical bar volume)", min = 0.001, max = 2, step = 0.001)
thr = param.float(2.0, title = "Extreme ratio", min = 1.1, max = 10, step = 0.1)
q = ta.sma(volume, 20) * frac
bi = book.impact(q, book.ask_side)
si = book.impact(q, book.bid_side)
r = math.log(math.max(si, 1e-9) / math.max(bi, 1e-9))
t = math.clamp(math.abs(r) / math.log(thr), 0, 1)
render.candle_color(math.is_na(r) ? na : r > 0 ? color.gradient(t, 0, 1, color.slate.500, color.emerald.400) : color.gradient(t, 0, 1, color.slate.500, color.rose.400))
render.mark(r > math.log(thr) and r[1] <= math.log(thr), shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.300, size = 0.5, title = "Buying much cheaper")
render.mark(r < -math.log(thr) and r[1] >= -math.log(thr), shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.300, size = 0.5, title = "Selling much cheaper")Impact Cost Monitor
analysis · presets/analysis/book/impact_cost_monitor.qs · uses viz/premium
What it would cost right now to trade a slice of a typical bar's volume (10 % by default), from the actual book, against what the bars say liquidity has been. Buy impact and sell impact are the distance (in basis points) from the mid to the average fill of a market order of that size walking the asks or the bids. The baseline is Amihud illiquidity from OHLCV (absolute return per unit of traded value), scaled to the same units over its own history. Book impact far above the bar-based baseline = the book is thinner than the tape suggests (liquidity has been pulled); the gap between buy and sell impact shows which side is thin. Needs the order book (L2).
analysis "Impact Cost Monitor" {
pane = hpane
height = 0.22
render = webgpu
}
/// What it would cost right now to trade a slice of a typical bar's volume (10 % by default), from the actual book, against what
/// the bars say liquidity has been. Buy impact and sell impact are the distance (in basis points)
/// from the mid to the average fill of a market order of that size walking the asks
/// or the bids. The baseline is Amihud illiquidity from OHLCV (absolute return per unit of
/// traded value), scaled to the same units over its own history.
/// Book impact far above the bar-based baseline = the book is thinner than the tape suggests
/// (liquidity has been pulled); the gap between buy and sell impact shows which side is thin.
/// Needs the order book (L2).
use "viz/premium" as pr
vol_len = param.int(20, title = "Typical volume (bars)", min = 2, max = 500)
frac = param.float(0.1, title = "Order size (x typical bar volume)", min = 0.005, max = 5, step = 0.005)
warn = param.float(2.0, title = "Thin-book warning (x baseline)", min = 1.1, max = 10, step = 0.1)
q = ta.sma(volume, vol_len) * frac
buy_bps = book.impact(q, book.ask_side) * 100
sell_bps = book.impact(q, book.bid_side) * 100
ami = hft.amihud(vol_len, 1)
// baseline: Amihud scaled so that its median over the window matches the median book impact
avg_bps = (buy_bps + sell_bps) / 2
scale = stat.percentile(avg_bps, 200, 50) / math.max(stat.percentile(ami, 200, 50), 1e-18)
base = ami * scale
pb = render.line(buy_bps, "Buy impact (bps)", color = color.rose.300, width = 1.6)
ps = render.line(sell_bps, "Sell impact (bps)", color = color.emerald.300, width = 1.6)
render.band(pb, ps, color = buy_bps > sell_bps ? color.rose.500 : color.emerald.500, opacity = 0.18, title = "Asymmetry")
render.line(base, "Bar baseline (Amihud)", color = color.translucent(color.slate.300, 35), width = 1, stroke = style.dashed)
thin = avg_bps > warn * base
render.background(thin ? color.translucent(color.amber.500, 88) : na)
alert.rule(thin and not thin[1], "Thin book", "Book impact is well above the bar-based liquidity baseline")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Impact Cost", "Order book × OHLCV")
pr.row(t, 2, "Buy / sell impact", math.is_na(buy_bps) ? "book too thin" : str.format("{:.1f} / {:.1f} bps", buy_bps, sell_bps), color.white)
pr.row(t, 3, "vs baseline", str.format("{:.1f}x", avg_bps / math.max(base, 1e-12)), thin ? pr.WARN : pr.BULL)
}Liquidity Vacuum
analysis · presets/analysis/book/liquidity_vacuum.qs
Moves travel fastest where nothing rests in the book. The resting size within a band around the mid is compared with its own history (z-score of its logarithm): a strongly negative score is a vacuum, shaded on the chart. A range breakout (OHLCV) that starts inside a vacuum is marked, since there is little liquidity to stop it; one that starts into a thick book is marked faint. Needs the order book (L2).
analysis "Liquidity Vacuum" {
overlay = true
short_title = "Vacuum"
}
/// Moves travel fastest where nothing rests in the book. The resting size within a band around the
/// mid is compared with its own history (z-score of its logarithm): a strongly negative score is a
/// vacuum, shaded on the chart. A range breakout (OHLCV) that starts inside a vacuum is marked,
/// since there is little liquidity to stop it; one that starts into a thick book is marked faint.
/// Needs the order book (L2).
pct = param.float(0.5, title = "Band around mid (%)", min = 0.02, max = 5, step = 0.01)
z_len = param.int(200, title = "History (bars)", min = 20, max = 5000)
z_vac = param.float(-1.2, title = "Vacuum z-score", min = -4, max = 0, step = 0.1)
brk = param.int(20, title = "Breakout range (bars)", min = 3, max = 500)
liq = book.liquidity(pct)
z = stat.zscore(math.log(math.max(liq, 1e-12)), z_len)
vac = z < z_vac
thick = z > -z_vac
render.background(vac ? color.translucent(color.violet.500, 85) : thick ? color.translucent(color.slate.500, 94) : na)
hh = ta.highest(high, brk)
ll = ta.lowest(low, brk)
up_b = close > hh[1]
dn_b = close < ll[1]
render.mark((up_b or dn_b) and vac, shape = shape.star, at = anchor.above_bar, color = color.violet.300, size = 0.9, title = "Breakout into a vacuum")
render.mark((up_b or dn_b) and thick, shape = shape.circle, at = anchor.above_bar, color = color.translucent(color.slate.300, 40), size = 0.5, title = "Breakout into a thick book")
alert.rule((up_b or dn_b) and vac, "Vacuum breakout", "A range breakout started in a liquidity vacuum")Liquidity Walls
analysis · presets/analysis/book/liquidity_walls.qs · uses viz/premium
The largest resting bid and ask near the price, bar by bar: the levels where size is waiting. Within a window around the mid, the biggest level of each side is a wall when it is several times the average level size of that side. Walls are drawn as stepped lines whose width grows with the wall's size; a wall that holds for several bars becomes a zone. OHLCV decides what happened at the wall: a bar that trades into a bid wall and closes back above it = wall defended; a wall that disappears as price comes within reach = pulled (a spoof or a participant stepping away); a close through a wall that was there = wall consumed. Needs the order book (L2).
analysis "Liquidity Walls" {
overlay = true
render = webgpu
short_title = "Walls"
max_objects = 80
}
/// The largest resting bid and ask near the price, bar by bar: the levels where size is waiting.
/// Within a window around the mid, the biggest level of each side is a wall when it is several
/// times the average level size of that side. Walls are drawn as stepped lines whose width grows
/// with the wall's size; a wall that holds for several bars becomes a zone.
/// OHLCV decides what happened at the wall: a bar that trades into a bid wall and closes back above
/// it = wall defended; a wall that disappears as price comes within reach = pulled (a spoof or a
/// participant stepping away); a close through a wall that was there = wall consumed.
/// Needs the order book (L2).
use "viz/premium" as pr
win_pct = param.float(1.0, title = "Search window (% from mid)", min = 0.05, max = 10, step = 0.05, group = "Walls")
mult = param.float(3.0, title = "Wall size (x average level)", min = 1.2, max = 20, step = 0.1, group = "Walls")
reach = param.float(0.5, title = "Within reach (ATR)", min = 0.1, max = 3, step = 0.1, group = "Signals")
hold = param.int(5, title = "Bars for a zone", min = 2, max = 100, group = "Signals")
bids = book.bids
asks = book.asks
mid = book.mid
bw = na
bws = 0.0
bsum = 0.0
bn = 0
for i in 0..grid.rows(bids) {
px = grid.get(bids, i, book.col_price)
sz = grid.get(bids, i, book.col_size)
if px >= mid * (1 - win_pct / 100) {
bsum := bsum + sz
bn := bn + 1
if sz > bws {
bws := sz
bw := px
}
}
}
aw = na
aws = 0.0
asum = 0.0
an = 0
for i in 0..grid.rows(asks) {
px = grid.get(asks, i, book.col_price)
sz = grid.get(asks, i, book.col_size)
if px <= mid * (1 + win_pct / 100) {
asum := asum + sz
an := an + 1
if sz > aws {
aws := sz
aw := px
}
}
}
bid_wall = bn > 2 and bws >= mult * bsum / bn ? bw : na
ask_wall = an > 2 and aws >= mult * asum / an ? aw : na
bid_rel = bn > 0 ? bws / (bsum / bn) : na
ask_rel = an > 0 ? aws / (asum / an) : na
render.step(bid_wall, "Bid wall", color = color.gradient(bid_rel, mult, mult * 3, color.emerald.700, color.emerald.300), width = 2.5)
render.step(ask_wall, "Ask wall", color = color.gradient(ask_rel, mult, mult * 3, color.rose.700, color.rose.300), width = 2.5)
a = ta.atr(14)
defended_bid = not math.is_na(bid_wall[1]) and low <= bid_wall[1] and close > bid_wall[1]
defended_ask = not math.is_na(ask_wall[1]) and high >= ask_wall[1] and close < ask_wall[1]
pulled_bid = not math.is_na(bid_wall[1]) and math.is_na(bid_wall) and low > bid_wall[1] and low - bid_wall[1] < reach * a
pulled_ask = not math.is_na(ask_wall[1]) and math.is_na(ask_wall) and high < ask_wall[1] and ask_wall[1] - high < reach * a
eaten_bid = not math.is_na(bid_wall[1]) and close < bid_wall[1]
eaten_ask = not math.is_na(ask_wall[1]) and close > ask_wall[1]
render.mark(defended_bid, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Bid wall defended")
render.mark(defended_ask, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Ask wall defended")
render.mark(pulled_bid or pulled_ask, shape = shape.x, at = anchor.above_bar, color = color.amber.300, size = 0.6, title = "Wall pulled")
render.mark(eaten_bid, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.400, size = 0.7, title = "Bid wall consumed")
render.mark(eaten_ask, shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.400, size = 0.7, title = "Ask wall consumed")
alert.rule(eaten_bid or eaten_ask, "Wall consumed", "Price closed through a liquidity wall")
alert.rule(pulled_bid or pulled_ask, "Wall pulled", "A liquidity wall vanished as price came within reach")
// persistent walls become zones
bid_age = math.is_na(bid_wall) ? 0 : math.abs(bid_wall - math.coalesce(bid_wall[1], bid_wall)) <= a * 0.1 ? 1 : 0
persist bid_run = 0
persist ask_run = 0
bid_run := math.is_na(bid_wall) ? 0 : bid_age == 1 ? bid_run + 1 : 1
ask_run := math.is_na(ask_wall) ? 0 : (not math.is_na(ask_wall[1]) and math.abs(ask_wall - ask_wall[1]) <= a * 0.1) ? ask_run + 1 : 1
if bid_run == hold {
draw.zone.create(bar.index - hold + 1, bid_wall + 0.1 * a, bar.index + 20, bid_wall - 0.1 * a, color = color.translucent(color.emerald.500, 80), border_color = color.emerald.400, text = "Bid wall")
}
if ask_run == hold {
draw.zone.create(bar.index - hold + 1, ask_wall + 0.1 * a, bar.index + 20, ask_wall - 0.1 * a, color = color.translucent(color.rose.500, 80), border_color = color.rose.400, text = "Ask wall")
}
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Liquidity Walls", "Order book × OHLCV")
pr.row(t, 2, "Bid wall", math.is_na(bid_wall) ? "none" : str.format("{:.2f} · {:.0f} ({:.1f}x)", bid_wall, bws, bid_rel), pr.BULL)
pr.row(t, 3, "Ask wall", math.is_na(ask_wall) ? "none" : str.format("{:.2f} · {:.0f} ({:.1f}x)", ask_wall, aws, ask_rel), pr.BEAR)
}Microprice Edge
analysis · presets/analysis/book/microprice_edge.qs
The microprice leads the mid: it leans towards the side with less size at the touch, where the next tick is likely to go. This pane shows that lean in spreads ((microprice - mid) / spread, -0.5 .. 0.5) as columns, the cumulative order-flow imbalance of the top five levels (Cont-Kukanov-Stoikov, normalised by its own volatility), and the quoted spread against the bar's ATR, which says whether the edge is worth more than the cost of crossing. Signals: edge and order-flow imbalance agree while the spread is small against the range. Needs the order book (L1 for the edge, L2 for the order-flow imbalance).
analysis "Microprice Edge" {
pane = hpane
height = 0.22
render = webgpu
}
/// The microprice leads the mid: it leans towards the side with less size at the touch, where the
/// next tick is likely to go. This pane shows that lean in spreads ((microprice - mid) / spread,
/// -0.5 .. 0.5) as columns, the cumulative order-flow imbalance of the top five levels
/// (Cont-Kukanov-Stoikov, normalised by its own volatility), and the quoted spread against the
/// bar's ATR, which says whether the edge is worth more than the cost of crossing.
/// Signals: edge and order-flow imbalance agree while the spread is small against the range.
/// Needs the order book (L1 for the edge, L2 for the order-flow imbalance).
ofi_len = param.int(50, title = "OFI normalisation", min = 5, max = 1000)
thr = param.float(0.2, title = "Edge threshold (spreads)", min = 0.02, max = 0.5, step = 0.01)
max_sp = param.float(0.1, title = "Max spread (x ATR)", min = 0.005, max = 1, step = 0.005)
sp = book.spread
edge = (book.microprice - book.mid) / math.max(sp, 1e-12)
ofi = orderbook.ofi(5)
ofi_z = stat.sum(math.coalesce(ofi, 0), 10) / math.max(stat.stdev(math.coalesce(ofi, 0), ofi_len) * math.sqrt(10), 1e-12)
cost = sp / math.max(ta.atr(14), 1e-12)
render.histogram(edge, "Microprice edge", color = edge >= 0 ? color.cyan.400 : color.pink.400, width = 1)
render.line(math.clamp(ofi_z, -3, 3) / 6, "OFI z (scaled)", color = color.amber.300, width = 1.6)
render.line(math.min(cost / max_sp, 2) * 0.25, "Spread / ATR (scaled)", color = color.translucent(color.slate.300, 30), width = 1)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))
long_edge = edge > thr and ofi_z > 1 and cost < max_sp
short_edge = edge < -thr and ofi_z < -1 and cost < max_sp
render.mark(long_edge and not long_edge[1], shape = shape.triangle_up, at = anchor.bottom, color = color.cyan.300, title = "Buy-side edge")
render.mark(short_edge and not short_edge[1], shape = shape.triangle_down, at = anchor.top, color = color.pink.300, title = "Sell-side edge")
alert.rule(long_edge or short_edge, "Microprice edge", "Microprice and order-flow imbalance agree on a cheap spread")Microprice Ribbon
analysis · presets/analysis/book/microprice_ribbon.qs
Three fair prices from the book drawn as a ribbon: the mid, the microprice (touch sizes) and the depth-weighted mid of the top levels. When the microprice and the depth mid sit above the mid, the book is about to lift; below, to drop. The ribbon colour follows their agreement and the close is compared with it to show whether the bar finished on the book's side. Needs the order book (L1 for the microprice, L2 for the depth mid).
analysis "Microprice Ribbon" {
overlay = true
render = webgpu
short_title = "MPR"
}
/// Three fair prices from the book drawn as a ribbon: the mid, the microprice (touch sizes) and the
/// depth-weighted mid of the top levels. When the microprice and the depth mid sit above the mid,
/// the book is about to lift; below, to drop. The ribbon colour follows their agreement and the
/// close is compared with it to show whether the bar finished on the book's side.
/// Needs the order book (L1 for the microprice, L2 for the depth mid).
levels = param.int(5, title = "Depth levels", min = 1, max = 100)
smooth = param.int(2, title = "Smoothing", min = 1, max = 30)
mid = ta.ema(book.mid, smooth)
micro = ta.ema(book.microprice, smooth)
bv = book.vwap(book.bid_side, levels)
av = book.vwap(book.ask_side, levels)
bq = book.bid_volume(levels)
aq = book.ask_volume(levels)
dmid = ta.ema((bv * aq + av * bq) / math.max(bq + aq, 1e-12), smooth)
lean = (micro - mid) + (dmid - mid)
pm = render.line(mid, "Mid", color = color.translucent(color.slate.300, 40), width = 1)
pmi = render.line(micro, "Microprice", color = color.cyan.300, width = 1.5)
pd = render.line(dmid, "Depth mid", color = color.violet.300, width = 1.5)
render.band(pmi, pm, color = lean >= 0 ? color.emerald.500 : color.rose.500, opacity = 0.3, title = "Lean")
render.band(pd, pm, color = lean >= 0 ? color.emerald.400 : color.rose.400, opacity = 0.15, title = "Depth lean")
render.candle_color(math.is_na(lean) ? na : close > open and lean > 0 ? color.emerald.400 : close < open and lean < 0 ? color.rose.400 : na)Crowd vs Size Imbalance
analysis · presets/analysis/book/order_count_imbalance.qs
Two imbalances of the same book: by size (who has more quantity resting) and by order count (who has more participants). Size-heavy and count-light on one side means a few large orders; count-heavy and size-light means many small ones. When the crowd and the size disagree, the large orders have usually been the better guide. Uses the order counts L2 feeds report (Zerodha, Fyers) or individual L3 orders; the count line is na otherwise.
analysis "Crowd vs Size Imbalance" {
pane = hpane
height = 0.2
render = webgpu
}
/// Two imbalances of the same book: by size (who has more quantity resting) and by order count
/// (who has more participants). Size-heavy and count-light on one side means a few large orders;
/// count-heavy and size-light means many small ones. When the crowd and the size disagree, the
/// large orders have usually been the better guide. Uses the order counts L2 feeds report
/// (Zerodha, Fyers) or individual L3 orders; the count line is na otherwise.
levels = param.int(10, title = "Levels", min = 1, max = 200)
size_imb = book.imbalance(levels)
bn = book.order_count(book.bid_side)
an = book.order_count(book.ask_side)
cnt_imb = (bn + an) > 0 ? (bn - an) / (bn + an) : na
render.histogram(size_imb, "Size imbalance", color = size_imb >= 0 ? color.teal.400 : color.rose.400, width = 1)
render.line(cnt_imb, "Order-count imbalance", color = color.amber.300, width = 1.8)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))
whales_bid = size_imb > 0.2 and cnt_imb < 0
whales_ask = size_imb < -0.2 and cnt_imb > 0
render.background(whales_bid ? color.translucent(color.teal.500, 88) : whales_ask ? color.translucent(color.rose.500, 88) : na)
alert.rule(whales_bid or whales_ask, "Size vs crowd", "Large orders lean against the crowd")Order Size Profile
analysis · presets/analysis/book/order_size_profile.qs · uses viz/premium
Who is resting in the book: many small orders or a few large ones. The average order size of each side is its resting size divided by its order count (individual orders with an L3 feed, the per-level order counts of L2 feeds such as Zerodha and Fyers). A side whose average order suddenly grows has large participants arriving; a large single order (L3) is marked. Book turnover compares the bar's traded volume (OHLCV) with the resting size near the touch: high turnover with stable large orders is a sign of refilled (iceberg-like) liquidity. Needs order counts (L2 with counts) or individual orders (L3); shows na otherwise.
analysis "Order Size Profile" {
pane = hpane
height = 0.22
render = webgpu
}
/// Who is resting in the book: many small orders or a few large ones. The average order size of
/// each side is its resting size divided by its order count (individual orders with an L3 feed,
/// the per-level order counts of L2 feeds such as Zerodha and Fyers). A side whose average order
/// suddenly grows has large participants arriving; a large single order (L3) is marked.
/// Book turnover compares the bar's traded volume (OHLCV) with the resting size near the touch:
/// high turnover with stable large orders is a sign of refilled (iceberg-like) liquidity.
/// Needs order counts (L2 with counts) or individual orders (L3); shows na otherwise.
use "viz/premium" as pr
levels = param.int(10, title = "Levels counted", min = 1, max = 200)
z_len = param.int(100, title = "Z-score window", min = 10, max = 2000)
big_z = param.float(2.5, title = "Large-order z-score", min = 1, max = 6, step = 0.1)
bn = book.order_count(book.bid_side)
an = book.order_count(book.ask_side)
bavg = book.bid_volume() / math.max(bn, 1)
aavg = book.ask_volume() / math.max(an, 1)
bz = stat.zscore(math.log(math.max(bavg, 1e-12)), z_len)
az = stat.zscore(math.log(math.max(aavg, 1e-12)), z_len)
turn = volume / math.max(book.bid_volume(levels) + book.ask_volume(levels), 1e-12)
render.histogram(bz, "Bid order size z", color = color.translucent(color.emerald.400, 30), width = 1)
render.histogram(-az, "Ask order size z", color = color.translucent(color.rose.400, 30), width = 1)
render.line(math.min(turn, 5), "Book turnover", color = color.amber.300, width = 1.6)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))
(lp, ls) = book.largest_order()
big_bid = bz > big_z
big_ask = az > big_z
render.mark(big_bid and not big_bid[1], shape = shape.triangle_up, at = anchor.bottom, color = color.emerald.300, title = "Large bids arrive")
render.mark(big_ask and not big_ask[1], shape = shape.triangle_down, at = anchor.top, color = color.rose.300, title = "Large asks arrive")
alert.rule(big_bid or big_ask, "Large orders", "The average resting order size jumped on one side")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
pr.heading(t, 2, "Order Size Profile", "Order book × OHLCV")
pr.row(t, 2, "Avg bid / ask order", math.is_na(bn) ? "no order counts" : str.format("{:.1f} / {:.1f}", bavg, aavg), color.white)
pr.row(t, 3, "Orders bid / ask", math.is_na(bn) ? "-" : str.format("{:.0f} / {:.0f}", bn, an), color.white)
pr.row(t, 4, "Largest order (L3)", math.is_na(ls) ? "needs L3" : str.format("{:.0f} @ {:.2f}", ls, lp), color.white)
}Queue Depletion
analysis · presets/analysis/book/queue_depletion.qs
What happened to the queues at the touch between consecutive bars. When the best bid stays at the same price, the change in its size is queue growth (new bids joining) or depletion (bids filled or cancelled); a higher best bid counts as its full size added, a lower one as the old queue removed. The same for the ask, sign reversed. The columns are the net (bid flow - ask flow) in units of average touch size; the line is its cumulative sum with a decay. A falling price with bid queues still growing = support building; a rising price while ask queues refill = supply. Needs the order book (L1).
analysis "Queue Depletion" {
pane = hpane
height = 0.22
render = webgpu
}
/// What happened to the queues at the touch between consecutive bars. When the best bid stays at
/// the same price, the change in its size is queue growth (new bids joining) or depletion (bids
/// filled or cancelled); a higher best bid counts as its full size added, a lower one as the old
/// queue removed. The same for the ask, sign reversed. The columns are the net (bid flow - ask
/// flow) in units of average touch size; the line is its cumulative sum with a decay.
/// A falling price with bid queues still growing = support building; a rising price while ask
/// queues refill = supply. Needs the order book (L1).
decay = param.float(0.9, title = "Memory decay", min = 0.5, max = 0.999, step = 0.005)
bp = book.bid
bq = book.bid_size
ap = book.ask
aq = book.ask_size
bid_flow = math.is_na(bp[1]) ? na : bp > bp[1] ? bq : bp < bp[1] ? -bq[1] : bq - bq[1]
ask_flow = math.is_na(ap[1]) ? na : ap < ap[1] ? aq : ap > ap[1] ? -aq[1] : aq - aq[1]
avg_q = math.max(ta.sma((bq + aq) / 2, 50), 1e-12)
net = (bid_flow - ask_flow) / avg_q
persist mem = 0.0
mem := math.is_na(net) ? mem * decay : mem * decay + net
render.histogram(net, "Net queue flow", color = net >= 0 ? color.teal.400 : color.rose.400, width = 1)
render.line(mem, "Queue memory", color = color.amber.300, width = 1.8)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))
support = close < close[3] and mem > 0 and mem > mem[3]
supply = close > close[3] and mem < 0 and mem < mem[3]
render.mark(support and not support[1], shape = shape.triangle_up, at = anchor.bottom, color = color.teal.300, title = "Bids building into a decline")
render.mark(supply and not supply[1], shape = shape.triangle_down, at = anchor.top, color = color.rose.300, title = "Asks building into a rally")
alert.rule(support or supply, "Queue divergence", "Touch queues build against the price move")Spoof Monitor
analysis · presets/analysis/book/spoof_monitor.qs
Large orders that appear and vanish without trading. A wall is the largest level of a side when it is several times the side's average level. A wall that disappears within a few bars while price never traded to its price (from the bars' high and low) was pulled, not filled: a spoof candidate. The columns count pulled walls per side with a decay; persistent one-sided pulling is a warning that the visible book on that side is not real. Needs the order book (L2).
analysis "Spoof Monitor" {
pane = hpane
height = 0.2
render = webgpu
}
/// Large orders that appear and vanish without trading. A wall is the largest level of a side when
/// it is several times the side's average level. A wall that disappears within a few bars while
/// price never traded to its price (from the bars' high and low) was pulled, not filled: a spoof
/// candidate. The columns count pulled walls per side with a decay; persistent one-sided pulling
/// is a warning that the visible book on that side is not real.
/// Needs the order book (L2).
mult = param.float(4.0, title = "Wall size (x average level)", min = 1.5, max = 30, step = 0.5)
life = param.int(5, title = "Pulled within (bars)", min = 1, max = 100)
decay = param.float(0.95, title = "Count decay", min = 0.5, max = 0.999, step = 0.005)
(bwp, bws) = book.wall(book.bid_side)
(awp, aws) = book.wall(book.ask_side)
bavg = book.bid_volume() / math.max(book.levels, 1)
aavg = book.ask_volume() / math.max(book.levels, 1)
bid_wall = bws >= mult * bavg ? bwp : na
ask_wall = aws >= mult * aavg ? awp : na
persist b_px = na
persist b_age = 0
persist a_px = na
persist a_age = 0
persist b_cnt = 0.0
persist a_cnt = 0.0
b_cnt := b_cnt * decay
a_cnt := a_cnt * decay
pulled_b = false
pulled_a = false
if not math.is_na(b_px) {
traded = low <= b_px
gone = math.is_na(bid_wall) or math.abs(bid_wall - b_px) > 1e-9
if traded { b_px := na }
else if gone and b_age <= life {
pulled_b := true
b_cnt := b_cnt + 1
b_px := na
}
}
if not math.is_na(a_px) {
traded = high >= a_px
gone = math.is_na(ask_wall) or math.abs(ask_wall - a_px) > 1e-9
if traded { a_px := na }
else if gone and a_age <= life {
pulled_a := true
a_cnt := a_cnt + 1
a_px := na
}
}
if math.is_na(b_px) and not math.is_na(bid_wall) {
b_px := bid_wall
b_age := 0
}
if math.is_na(a_px) and not math.is_na(ask_wall) {
a_px := ask_wall
a_age := 0
}
b_age += 1
a_age += 1
render.histogram(b_cnt, "Pulled bid walls", color = color.translucent(color.emerald.400, 30), width = 1)
render.histogram(-a_cnt, "Pulled ask walls", color = color.translucent(color.rose.400, 30), width = 1)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))
render.mark(pulled_b, shape = shape.x, at = anchor.bottom, color = color.emerald.300, size = 0.5, title = "Bid wall pulled")
render.mark(pulled_a, shape = shape.x, at = anchor.top, color = color.rose.300, size = 0.5, title = "Ask wall pulled")
alert.rule(pulled_b or pulled_a, "Wall pulled", "A large order vanished without price trading to it")Spread Regime
analysis · presets/analysis/book/spread_regime.qs · uses viz/premium
The quoted spread of the real book against the spreads the bars imply. Quoted is best ask minus best bid at each bar's close, in basis points; the Roll and Corwin-Schultz estimates are the effective spreads that OHLCV alone suggests. Range per spread (ATR / quoted spread) says how many spreads a typical bar moves: below a few, crossing the spread eats most of any edge. Background: wide regime (quoted spread above its 80th percentile) in amber, tight regime in teal. Needs the order book (L1 is enough).
analysis "Spread Regime" {
pane = hpane
height = 0.2
render = webgpu
}
/// The quoted spread of the real book against the spreads the bars imply. Quoted is best ask minus
/// best bid at each bar's close, in basis points; the Roll and Corwin-Schultz estimates are the
/// effective spreads that OHLCV alone suggests. Range per spread (ATR / quoted spread) says how many
/// spreads a typical bar moves: below a few, crossing the spread eats most of any edge.
/// Background: wide regime (quoted spread above its 80th percentile) in amber, tight regime in teal.
/// Needs the order book (L1 is enough).
use "viz/premium" as pr
pct_len = param.int(300, title = "Percentile window", min = 20, max = 5000)
q_bps = book.spread / math.max(book.mid, 1e-12) * 10000
roll = hft.roll_spread(20) / math.max(close, 1e-12) * 10000
cs = hft.corwin_schultz(20) * 10000
rps = ta.atr(14) / math.max(book.spread, 1e-12)
render.line(q_bps, "Quoted spread (bps)", color = color.sky.300, width = 1.8)
render.line(roll, "Roll estimate (bps)", color = color.translucent(color.violet.300, 30), width = 1)
render.line(cs, "Corwin-Schultz (bps)", color = color.translucent(color.pink.300, 30), width = 1)
render.line(math.min(rps, 100) / 10, "Range per spread / 10", color = color.amber.300, width = 1.2, stroke = style.dotted)
hi = stat.percentile(q_bps, pct_len, 80)
lo = stat.percentile(q_bps, pct_len, 20)
wide = q_bps > hi
tight = q_bps < lo
render.background(wide ? color.translucent(color.amber.500, 88) : tight ? color.translucent(color.teal.500, 90) : na)
alert.rule(wide and not wide[1], "Wide spread", "The quoted spread entered its top quintile")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
pr.heading(t, 2, "Spread Regime", "Order book × OHLCV")
pr.row(t, 2, "Quoted spread", math.is_na(q_bps) ? "no book" : str.format("{:.2f} bps", q_bps), color.white)
pr.row(t, 3, "Range per spread", math.is_na(rps) ? "-" : str.format("{:.1f}", rps), rps < 3 ? pr.WARN : pr.BULL)
}Touch Imbalance Candles
analysis · presets/analysis/book/touch_imbalance_candles.qs
Candles shaded by the top-of-book imbalance at their close: (bid size - ask size) / (bid size + ask size) at the best prices, a short-term predictor of the next tick. Strong bid-side imbalance shades teal, strong ask-side rose, with intensity by strength. Works with L1 data (quotes); bars without a book keep their colours.
analysis "Touch Imbalance Candles" {
overlay = true
short_title = "TIC"
}
/// Candles shaded by the top-of-book imbalance at their close: (bid size - ask size) / (bid size +
/// ask size) at the best prices, a short-term predictor of the next tick. Strong bid-side
/// imbalance shades teal, strong ask-side rose, with intensity by strength. Works with L1 data
/// (quotes); bars without a book keep their colours.
s = param.float(0.6, title = "Full colour at", min = 0.1, max = 1, step = 0.05)
bq = book.bid_size
aq = book.ask_size
imb = (bq + aq) > 0 ? (bq - aq) / (bq + aq) : na
t = math.clamp(math.abs(imb) / s, 0, 1)
render.candle_color(math.is_na(imb) ? na : imb >= 0 ? color.gradient(t, 0, 1, color.slate.500, color.teal.300) : color.gradient(t, 0, 1, color.slate.500, color.rose.400))
render.mark(imb > s and close < open, shape = shape.circle, at = anchor.below_bar, color = color.teal.300, size = 0.4, title = "Down bar, bids stacked")
render.mark(imb < -s and close > open, shape = shape.circle, at = anchor.above_bar, color = color.rose.300, size = 0.4, title = "Up bar, asks stacked")Wall Distance Oscillator
analysis · presets/analysis/book/wall_distance_oscillator.qs
How far the nearest large walls are, in ATR, and which one is closer. The bid wall distance is plotted below zero and the ask wall distance above; the oscillator is their normalised balance: positive = the ask wall is further away than the bid wall (room to rise, support close by). Needs the order book (L2).
analysis "Wall Distance Oscillator" {
pane = hpane
height = 0.2
render = webgpu
}
/// How far the nearest large walls are, in ATR, and which one is closer. The bid wall distance is
/// plotted below zero and the ask wall distance above; the oscillator is their normalised balance:
/// positive = the ask wall is further away than the bid wall (room to rise, support close by).
/// Needs the order book (L2).
mult = param.float(3.0, title = "Wall size (x average level)", min = 1.2, max = 30, step = 0.1)
a = ta.atr(14)
(bwp, bws) = book.wall(book.bid_side)
(awp, aws) = book.wall(book.ask_side)
bavg = book.bid_volume() / math.max(book.levels, 1)
aavg = book.ask_volume() / math.max(book.levels, 1)
bd = bws >= mult * bavg ? (close - bwp) / math.max(a, 1e-12) : na
ad = aws >= mult * aavg ? (awp - close) / math.max(a, 1e-12) : na
osc = (ad - bd) / math.max(ad + bd, 1e-12)
render.histogram(ad, "Ask wall distance (ATR)", color = color.translucent(color.rose.400, 40), width = 1)
render.histogram(-bd, "Bid wall distance (ATR)", color = color.translucent(color.emerald.400, 40), width = 1)
render.line(osc, "Room balance", color = color.amber.300, width = 1.8)
render.level(0, "Zero", color = color.translucent(color.slate.500, 60))Wall Memory at Price
analysis · presets/analysis/book/wall_memory_at_price.qs
Where liquidity walls have stood, beside the price axis. Each bar adds the size of its largest bid (right, emerald) and largest ask (left, rose) within the search window at that wall's price, keeping the largest seen per row over the last 1 000 bars. Prices where big size keeps reappearing are the book's memory: support and resistance that the order book, not the chart pattern, created. The bid and ask walls of the newest bar are also drawn on the price pane. Needs the order book (L2).
analysis "Wall Memory at Price" {
pane = vpane
width = 150
}
/// Where liquidity walls have stood, beside the price axis. Each bar adds the size of its largest
/// bid (right, emerald) and largest ask (left, rose) within the search window at that wall's
/// price, keeping the largest seen per row over the last 1 000 bars. Prices where big size keeps
/// reappearing are the book's memory: support and resistance that the order book, not the chart
/// pattern, created. The bid and ask walls of the newest bar are also drawn on the price pane.
/// Needs the order book (L2).
win_pct = param.float(2.0, title = "Search window (% from mid)", min = 0.05, max = 20, step = 0.05)
mid = book.mid
(bwp, bws) = book.wall(book.bid_side)
(awp, aws) = book.wall(book.ask_side)
bid_px = bwp >= mid * (1 - win_pct / 100) ? bwp : na
ask_px = awp <= mid * (1 + win_pct / 100) ? awp : na
render.vpane.histogram(math.is_na(bid_px) ? na : bws, price = bid_px, period = "bars", bars = 1000, aggregate = "max", color = color.emerald.400, title = "Bid walls")
render.vpane.histogram(math.is_na(ask_px) ? na : -aws, price = ask_px, period = "bars", bars = 1000, aggregate = "min", color2 = color.rose.400, title = "Ask walls")
render.step(bid_px, "Bid wall", color = color.translucent(color.emerald.300, 30), width = 1.5)
render.step(ask_px, "Ask wall", color = color.translucent(color.rose.300, 30), width = 1.5)