
QuaScript Documentation
Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1
Microstructure
Asymmetric Risk Envelope
analysis · presets/analysis/hft/asymmetric_risk_envelope.qs
Risk bands that are not symmetric. Ordinary bands use one volatility for both sides; here the upper shells come from upside realized semivariance and the lower shells from downside semivariance, so the envelope leans the way risk actually leans: a selloff regime stretches the lower bands long before the upper ones move. Three shells per side (1, 2 and 3 sigma of the respective semivariance, scaled to the horizon) around an adaptive basis. The basis is colored by the semivariance balance, candles by where price sits in the envelope, and the dashboard reports the skew and the distance to each 2-sigma band.
analysis "Asymmetric Risk Envelope" {
overlay = true
render = webgpu
max_objects = 80
}
/// Risk bands that are not symmetric. Ordinary bands use one volatility for both sides; here the
/// upper shells come from upside realized semivariance and the lower shells from downside
/// semivariance, so the envelope leans the way risk actually leans: a selloff regime stretches
/// the lower bands long before the upper ones move. Three shells per side (1, 2 and 3 sigma of
/// the respective semivariance, scaled to the horizon) around an adaptive basis.
/// The basis is colored by the semivariance balance, candles by where price sits in the
/// envelope, and the dashboard reports the skew and the distance to each 2-sigma band.
src = param.source(close, title = "Source")
len = param.int(34, title = "Basis length", min = 5, max = 500, group = "Basis")
var_len = param.int(60, title = "Semivariance window", min = 10, max = 2000, group = "Risk")
horizon = param.int(10, title = "Horizon (bars)", min = 1, max = 500, group = "Risk")
color_bars = param.bool(true, title = "Color candles", group = "Style")
basis = ta.jurik(ta.ema(src, len), math.max(2, len / 3), 0, 2)
(up_v, dn_v, bal) = hft.semivariance(src, var_len)
// per-return semideviation (percent) -> price distance over the horizon; sqrt(2) restores the
// full-distribution scale of a one-sided sum
k = math.sqrt(2 * horizon / var_len) / 100
du = src * up_v * k
dd = src * dn_v * k
tone = bal >= 0 ? color.gradient(bal, 0, 0.6, color.slate.300, color.emerald.300) : color.gradient(bal, -0.6, 0, color.rose.400, color.slate.300)
b = render.line(basis, "Basis", color = tone, width = 2.2)
u1 = render.line(basis + du, "Upper 1", color = color.translucent(color.emerald.300, 60), width = 0.8)
u2 = render.line(basis + 2 * du, "Upper 2", color = color.translucent(color.emerald.300, 35), width = 1.1)
u3 = render.line(basis + 3 * du, "Upper 3", color = color.translucent(color.emerald.300, 55), width = 0.8, stroke = style.dotted)
l1 = render.line(basis - dd, "Lower 1", color = color.translucent(color.rose.300, 60), width = 0.8)
l2 = render.line(basis - 2 * dd, "Lower 2", color = color.translucent(color.rose.300, 35), width = 1.1)
l3 = render.line(basis - 3 * dd, "Lower 3", color = color.translucent(color.rose.300, 55), width = 0.8, stroke = style.dotted)
render.band(u1, b, color = color.translucent(color.emerald.400, 88), opacity = 1, title = "Upside core")
render.band(u2, u1, color = color.translucent(color.emerald.400, 92), opacity = 1, title = "Upside shell")
render.band(u3, u2, color = color.translucent(color.emerald.400, 95), opacity = 1, title = "Upside fringe")
render.band(b, l1, color = color.translucent(color.rose.500, 88), opacity = 1, title = "Downside core")
render.band(l1, l2, color = color.translucent(color.rose.500, 92), opacity = 1, title = "Downside shell")
render.band(l2, l3, color = color.translucent(color.rose.500, 95), opacity = 1, title = "Downside fringe")
pos = src >= basis ? (src - basis) / math.max(du, 1e-12) : -(basis - src) / math.max(dd, 1e-12)
render.candle_color(color_bars ? (pos >= 0 ? color.gradient(pos, 0, 3, color.slate.300, color.emerald.400) : color.gradient(pos, -3, 0, color.rose.500, color.slate.300)) : na)
render.mark(ta.cross_down(pos, -2), shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.400, title = "Downside break")
render.mark(ta.cross_up(pos, 2), shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, title = "Upside break")
render.mark(ta.cross_up(pos, -2), shape = shape.circle, at = anchor.below_bar, color = color.sky.300, size = 0.6, title = "Back inside (down side)")
skew_flip = bal < -0.3 and bal[1] >= -0.3
render.mark(skew_flip, shape = shape.diamond, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Risk turns to the downside")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Risk skew", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{:+.2f}", bal), bg_color = bal >= 0 ? color.gradient(bal, 0, 0.6, color.slate.700, color.emerald.700) : color.gradient(bal, -0.6, 0, color.rose.700, color.slate.700))
draw.table.cell(t, 1, 0, "To upper 2 sigma", text_color = color.slate.400)
draw.table.cell(t, 1, 1, str.format("{:+.2f}%", (basis + 2 * du - src) / src * 100))
draw.table.cell(t, 2, 0, "To lower 2 sigma", text_color = color.slate.400)
draw.table.cell(t, 2, 1, str.format("{:+.2f}%", (basis - 2 * dd - src) / src * 100))
draw.table.cell(t, 3, 0, "Position", text_color = color.slate.400)
draw.table.cell(t, 3, 1, str.format("{:+.2f} sigma", pos))
}
alert.rule(ta.cross_down(pos, -2) or ta.cross_up(pos, 2), "Envelope break", "Price closed beyond a 2-sigma semivariance band")Order Flow Imbalance Field
analysis · presets/analysis/hft/flow_imbalance_field.qs
Net order flow on twenty horizons (2 to ~230 bars) as a heatmap: the share of buy volume minus sell volume over each window, from the chart's real aggressor volume when the feed supplies it and bulk volume classification otherwise. Green rows are net buying, red rows net selling; a pressure wave that starts at the fast rows and climbs to the slow ones is a flow trend forming, a field that disagrees with price is absorption. The line is the cumulative delta of the window as a z-score; marks show flow divergences (price makes a new high or low that flow does not confirm).
analysis "Order Flow Imbalance Field" {
pane = hpane
height = 0.28
render = webgpu
}
/// Net order flow on twenty horizons (2 to ~230 bars) as a heatmap: the share of buy volume minus
/// sell volume over each window, from the chart's real aggressor volume when the feed supplies
/// it and bulk volume classification otherwise. Green rows are net buying, red rows net selling;
/// a pressure wave that starts at the fast rows and climbs to the slow ones is a flow trend
/// forming, a field that disagrees with price is absorption.
/// The line is the cumulative delta of the window as a z-score; marks show flow divergences
/// (price makes a new high or low that flow does not confirm).
first = param.int(2, title = "Shortest window", min = 1, max = 100, group = "Field")
ratio = param.float(1.28, title = "Window ratio", min = 1.05, max = 2, step = 0.01, group = "Field")
cvd_len = param.int(200, title = "Delta z-score window", min = 20, max = 5000, group = "Delta")
pivot = param.int(5, title = "Divergence pivot", min = 2, max = 50, group = "Delta")
fn heat(x: float) -> color {
t = math.clamp(math.abs(x) / 0.6, 0, 1)
return math.is_na(x) ? na : x >= 0 ? color.gradient(t, 0.05, 1, color.slate.900, color.emerald.300) : color.gradient(t, 0.05, 1, color.slate.900, color.red.400)
}
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
net = 0.0
buy_rows = 0
for k in 0..=19 {
n = math.max(1, math.round(first * math.pow(ratio, k)))
(b, s, imb) = hft.flow(n)
net := net + (math.is_na(imb) ? 0 : imb) / 20
if imb > 0 { buy_rows := buy_rows + 1 }
top = render.line(k + 0.5, str.format("Edge {}", k + 1), color = color.transparent, width = 0)
render.band(top, edge, color = heat(imb), opacity = 1, title = str.format("Row {}", k + 1))
edge := top
}
(bv, sv, imb1) = hft.flow(1)
persist cvd = 0.0
cvd := cvd + (math.is_na(bv) ? 0 : bv - sv)
cz = stat.zscore(cvd, cvd_len)
render.line(9.5 + math.clamp(cz, -3, 3) * 3, "Delta z-score", color = color.translucent(color.white, 20), width = 1.6)
(bull, bear) = ta.divergence(cvd, pivot, pivot, 80, false)
render.mark(bull, shape = shape.triangle_up, at = anchor.bottom, color = color.emerald.300, title = "Bullish flow divergence")
render.mark(bear, shape = shape.triangle_down, at = anchor.top, color = color.red.400, title = "Bearish flow divergence")
sweep_buy = buy_rows >= 18 and buy_rows[1] < 18
sweep_sell = buy_rows <= 2 and buy_rows[1] > 2
render.mark(sweep_buy, shape = shape.diamond, at = anchor.bottom, color = color.lime.300, size = 0.7, title = "All horizons buying")
render.mark(sweep_sell, shape = shape.diamond, at = anchor.top, color = color.rose.400, size = 0.7, title = "All horizons selling")
persist t = draw.table.create(anchor.top_right, 3, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Net flow", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{:+.0%}", net), bg_color = heat(net))
draw.table.cell(t, 1, 0, "Buying horizons", text_color = color.slate.400)
draw.table.cell(t, 1, 1, str.format("{} / 20", buy_rows))
draw.table.cell(t, 2, 0, "Delta z", text_color = color.slate.400)
draw.table.cell(t, 2, 1, str.format("{:+.2f}", cz))
}
alert.rule(bull or bear, "Flow divergence", "Cumulative delta does not confirm the price pivot")Order Flow Pressure
analysis · presets/analysis/hft/flow_pressure.qs
Buy volume above the zero line, sell volume mirrored below it, each column shaded by how unusual it is against its own history. The net imbalance is a spline, Hawkes intensity shows clustering of activity, and marks flag absorption: heavy one-sided volume that fails to move price.
analysis "Order Flow Pressure" {
pane = hpane
height = 0.28
}
/// Buy volume above the zero line, sell volume mirrored below it, each column shaded by how
/// unusual it is against its own history. The net imbalance is a spline, Hawkes intensity shows
/// clustering of activity, and marks flag absorption: heavy one-sided volume that fails to move
/// price.
win = param.int(20, title = "Window", min = 5, max = 200)
abs_z = param.float(1.6, title = "Absorption z-score", min = 0.5, max = 5, step = 0.1)
(buy, sell, imb) = hft.flow(win)
vb = buy / math.max(ta.sma(volume, 50), 1e-12)
vs = sell / math.max(ta.sma(volume, 50), 1e-12)
zb = stat.zscore(buy, 100)
zs = stat.zscore(sell, 100)
render.histogram(vb, "Buy volume", color = color.gradient(zb, -1, 3, color.teal.900, color.teal.300))
render.histogram(-vs, "Sell volume", color = color.gradient(zs, -1, 3, color.rose.900, color.rose.300))
render.spline(imb * 2, "Imbalance", color = imb >= 0 ? color.emerald.300 : color.pink.300, width = 2)
hk = hft.hawkes(volume > ta.sma(volume, 20) * 1.5, 0.15)
render.line(stat.zscore(hk, 200) * 0.3, "Activity clustering", color = color.translucent(color.yellow.300, 40), width = 1, stroke = style.dashed)
render.level(0, "Zero", color = color.slate.600)
move = math.abs(close - open) / math.max(ta.atr(14), 1e-12)
absorb_buy = zs > abs_z and move < 0.35
absorb_sell = zb > abs_z and move < 0.35
render.mark(absorb_buy, shape = shape.square, at = anchor.bottom, color = color.teal.300, size = 0.7, title = "Sell absorbed")
render.mark(absorb_sell, shape = shape.square, at = anchor.top, color = color.rose.300, size = 0.7, title = "Buy absorbed")
alert.rule(absorb_buy, "Sell absorption", "Heavy selling failed to move price")
alert.rule(absorb_sell, "Buy absorption", "Heavy buying failed to move price")Jump Diffusion Monitor
analysis · presets/analysis/hft/jump_diffusion_monitor.qs
Splits volatility into its continuous (diffusion) part and its jumps, the decomposition used by high-frequency risk desks. The filled area is realized volatility, the line inside it is bipower volatility (blind to jumps); the gap between them, the jump share, is shaded by size. Every return the Lee-Mykland test flags as a jump is marked, and the jumps feed two Hawkes intensities (up and down) so clusters of jumps, the signature of news shocks and liquidation cascades, show as the mirrored waves along the bottom. The baseline is the realized semivariance balance: upside variance against downside variance.
analysis "Jump Diffusion Monitor" {
pane = hpane
height = 0.3
render = webgpu
}
/// Splits volatility into its continuous (diffusion) part and its jumps, the decomposition used by
/// high-frequency risk desks. The filled area is realized volatility, the line inside it is
/// bipower volatility (blind to jumps); the gap between them, the jump share, is shaded by size.
/// Every return the Lee-Mykland test flags as a jump is marked, and the jumps feed two Hawkes
/// intensities (up and down) so clusters of jumps, the signature of news shocks and liquidation
/// cascades, show as the mirrored waves along the bottom.
/// The baseline is the realized semivariance balance: upside variance against downside variance.
src = param.source(close, title = "Source")
len = param.int(50, title = "Window (returns)", min = 10, max = 1000, group = "Decomposition")
alpha = param.float(0.01, title = "Jump test significance", min = 0.0001, max = 0.2, step = 0.001, group = "Jumps")
decay = param.float(0.08, title = "Cluster decay", min = 0.005, max = 1, step = 0.005, group = "Jumps")
(rv, bv, share) = hft.bipower(src, len)
(lm, jump) = hft.jump_test(src, len, alpha)
(up_v, dn_v, bal) = hft.semivariance(src, len)
hawk_up = hft.hawkes(jump > 0, decay, 1, 0)
hawk_dn = hft.hawkes(jump < 0, decay, 1, 0)
scale = math.max(ta.highest(rv, 200), 1e-12)
rv_n = rv / scale * 10 + 4
bv_n = bv / scale * 10 + 4
base = render.line(4, "Floor", color = color.translucent(color.slate.500, 60), width = 0.6)
r_line = render.line(rv_n, "Realized vol", color = color.amber.300, width = 1.6)
b_line = render.line(bv_n, "Diffusion vol", color = color.sky.300, width = 1.6)
render.band(b_line, base, color = color.translucent(color.sky.500, 80), opacity = 1, title = "Diffusion")
render.band(r_line, b_line, color = color.gradient(share, 0, 0.5, color.translucent(color.amber.400, 80), color.translucent(color.rose.500, 20)), opacity = 1, title = "Jump share")
mid = render.line(2, "Cluster axis", color = color.translucent(color.slate.500, 70), width = 0.5)
u = render.line(2 + math.min(hawk_up, 2) * 0.9, "Up cluster", color = color.emerald.400, width = 1)
d = render.line(2 - math.min(hawk_dn, 2) * 0.9, "Down cluster", color = color.rose.400, width = 1)
render.band(u, mid, color = color.translucent(color.emerald.500, 55), opacity = 1, title = "Up cluster fill")
render.band(mid, d, color = color.translucent(color.rose.500, 55), opacity = 1, title = "Down cluster fill")
render.line(15.5 + bal * 1.5, "Semivariance balance", color = bal >= 0 ? color.teal.300 : color.pink.400, width = 1.4)
render.mark(jump > 0, shape = shape.triangle_up, at = anchor.bottom, color = color.emerald.300, title = "Up jump")
render.mark(jump < 0, shape = shape.triangle_down, at = anchor.top, color = color.rose.400, title = "Down jump")
cascade = hawk_dn >= 2 and hawk_dn[1] < 2
render.mark(cascade, shape = shape.diamond, at = anchor.top, color = color.orange.400, title = "Down-jump cascade")
persist t = draw.table.create(anchor.top_right, 4, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Realized vol", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{:.2f}%", rv))
draw.table.cell(t, 1, 0, "Jump share", text_color = color.slate.400)
draw.table.cell(t, 1, 1, str.format("{:.0%}", share), bg_color = color.gradient(share, 0, 0.5, color.slate.700, color.rose.700))
draw.table.cell(t, 2, 0, "Up / down vol", text_color = color.slate.400)
draw.table.cell(t, 2, 1, str.format("{:.2f} / {:.2f}", up_v, dn_v))
draw.table.cell(t, 3, 0, "Jump clusters", text_color = color.slate.400)
draw.table.cell(t, 3, 1, str.format("{:.1f} up / {:.1f} down", hawk_up, hawk_dn))
}
alert.rule(jump != 0, "Price jump", "The Lee-Mykland test flagged this return as a jump")
alert.rule(cascade, "Jump cascade", "Down jumps are clustering (Hawkes intensity above 2)")Liquidity Cost Monitor
analysis · presets/analysis/hft/liquidity_cost.qs
How expensive it is to trade right now. Three spread and impact estimators from bars only (Roll, Corwin-Schultz and Amihud illiquidity) are normalised to their own percentile over a long window and averaged into a cost score drawn as a heat histogram. A high score means wide effective spreads and large price impact per unit of volume.
analysis "Liquidity Cost Monitor" {
pane = hpane
height = 0.24
}
/// How expensive it is to trade right now. Three spread and impact estimators from bars only
/// (Roll, Corwin-Schultz and Amihud illiquidity) are normalised to their own percentile over a
/// long window and averaged into a cost score drawn as a heat histogram. A high score means wide
/// effective spreads and large price impact per unit of volume.
len = param.int(20, title = "Estimator length", min = 5, max = 200)
rank = param.int(500, title = "Percentile window", min = 50, max = 5000)
roll = hft.roll_spread(len, close)
cs = hft.corwin_schultz(len)
ami = hft.amihud(len, 1000000)
p_roll = stat.percent_rank(roll, rank)
p_cs = stat.percent_rank(cs, rank)
p_ami = stat.percent_rank(ami, rank)
score = (math.coalesce(p_roll, 50) + math.coalesce(p_cs, 50) + math.coalesce(p_ami, 50)) / 3
heat = score >= 50 ? color.gradient(score, 50, 100, color.slate.700, color.rose.400) : color.gradient(score, 0, 50, color.emerald.400, color.slate.700)
render.histogram(score, "Cost score", base = 50, color = heat)
render.line(p_roll, "Roll spread rank", color = color.translucent(color.sky.300, 30), width = 1)
render.line(p_cs, "Corwin-Schultz rank", color = color.translucent(color.violet.300, 30), width = 1)
render.line(p_ami, "Amihud rank", color = color.translucent(color.amber.300, 30), width = 1)
render.spline(ta.ema(score, 10), "Trend", color = color.white, width = 2)
render.level(80, "Expensive", color = color.rose.800, stroke = style.dashed)
render.level(20, "Cheap", color = color.emerald.800, stroke = style.dashed)
render.background(score > 85 ? color.translucent(color.rose.500, 88) : na)
alert.rule(score > 85 and score[1] <= 85, "Liquidity drying up", "Spread and impact estimators are in their top 15%")Liquidity Stress Matrix
analysis · presets/analysis/hft/liquidity_stress_matrix.qs
Four independent estimates of trading cost and market depth, each on five horizons, as one heatmap of percentiles: Roll's effective spread, the Corwin-Schultz high-low spread, Amihud illiquidity and Kyle's lambda (price impact). Each estimator reads a different footprint of thin liquidity, so a stress event that shows on every block (bright across the matrix) is far more reliable than any single measure. Rows are grouped by estimator, short horizons first. The line is the matrix average; marks show a liquidity squeeze (most cells extreme) and the return to normal conditions.
analysis "Liquidity Stress Matrix" {
pane = hpane
height = 0.28
render = webgpu
}
/// Four independent estimates of trading cost and market depth, each on five horizons, as one
/// heatmap of percentiles: Roll's effective spread, the Corwin-Schultz high-low spread, Amihud
/// illiquidity and Kyle's lambda (price impact). Each estimator reads a different footprint of
/// thin liquidity, so a stress event that shows on every block (bright across the matrix) is far
/// more reliable than any single measure. Rows are grouped by estimator, short horizons first.
/// The line is the matrix average; marks show a liquidity squeeze (most cells extreme) and the
/// return to normal conditions.
history = param.int(300, title = "Percentile history", min = 50, max = 5000, group = "Matrix")
first = param.int(10, title = "Shortest horizon", min = 3, max = 200, group = "Matrix")
step_x = param.int(2, title = "Horizon multiplier", min = 2, max = 4, group = "Matrix")
fn heat(p: float) -> color {
return math.is_na(p) ? na : p >= 50 ? color.gradient(p, 50, 100, color.slate.900, color.yellow.300) : color.gradient(p, 0, 50, color.teal.900, color.slate.900)
}
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
sum = 0.0
cnt = 0
stressed = 0
for k in 0..=19 {
est = k / 5
h = first * math.pow(step_x, k % 5)
v = math.floor(est) == 0 ? hft.roll_spread(h) : math.floor(est) == 1 ? hft.corwin_schultz(h) : math.floor(est) == 2 ? hft.amihud(h) : hft.kyle_lambda(math.max(h, 10))
p = stat.percent_rank(v, history)
if not math.is_na(p) {
sum := sum + p
cnt := cnt + 1
if p >= 85 { stressed := stressed + 1 }
}
// a gap of 0.3 between estimator blocks
lo = k + math.floor(est) * 0.3
top = render.line(lo + 1, str.format("Edge {}", k + 1), color = color.transparent, width = 0)
render.band(top, edge, color = heat(p), opacity = 1, title = str.format("Row {}", k + 1))
edge := render.line(lo + 1 + (k % 5 == 4 ? 0.3 : 0), str.format("Gap {}", k + 1), color = color.transparent, width = 0)
}
avg = cnt > 0 ? sum / cnt : na
render.line(avg / 100 * 21, "Matrix average", color = color.translucent(color.white, 20), width = 1.6)
squeeze = stressed >= 12
render.mark(squeeze and not squeeze[1], shape = shape.triangle_down, at = anchor.top, color = color.yellow.300, title = "Liquidity squeeze")
render.mark(not squeeze and squeeze[1], shape = shape.circle, at = anchor.top, color = color.teal.300, size = 0.6, title = "Liquidity restored")
persist t = draw.table.create(anchor.top_right, 2, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Stressed cells", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{} / 20", stressed), bg_color = heat(stressed / 20 * 100))
draw.table.cell(t, 1, 0, "Matrix average", text_color = color.slate.400)
draw.table.cell(t, 1, 1, math.is_na(avg) ? "-" : str.format("{:.0f} pct", avg), bg_color = heat(avg))
}
alert.rule(squeeze and not squeeze[1], "Liquidity squeeze", "At least 12 of 20 liquidity cells are in their top 15%")Market Efficiency Field
analysis · presets/analysis/hft/market_efficiency_field.qs
Is price trending or mean-reverting, and on which time scale? Each of twenty-four rows is a Wald-Wolfowitz runs test on the signs of the last N price changes (N from 16 to ~370): green cells have fewer direction changes than chance (moves cluster, momentum works), magenta cells have more (moves alternate, fading works), dark cells are a random walk. Reading up the field shows the horizon at which the market stops being efficient, which is the horizon to trade. The line is the Lo-MacKinlay variance ratio (1 = random walk) as a cross-check.
analysis "Market Efficiency Field" {
pane = hpane
height = 0.26
render = webgpu
}
/// Is price trending or mean-reverting, and on which time scale? Each of twenty-four rows is a
/// Wald-Wolfowitz runs test on the signs of the last N price changes (N from 16 to ~370): green
/// cells have fewer direction changes than chance (moves cluster, momentum works), magenta cells
/// have more (moves alternate, fading works), dark cells are a random walk. Reading up the field
/// shows the horizon at which the market stops being efficient, which is the horizon to trade.
/// The line is the Lo-MacKinlay variance ratio (1 = random walk) as a cross-check.
src = param.source(close, title = "Source")
first = param.int(16, title = "Shortest window", min = 8, max = 200, group = "Field")
ratio = param.float(1.15, title = "Window ratio", min = 1.03, max = 1.5, step = 0.01, group = "Field")
vr_len = param.int(100, title = "Variance ratio window", min = 20, max = 2000, group = "Cross-check")
fn heat(z: float) -> color {
t = math.clamp(math.abs(z) / 3, 0, 1)
return math.is_na(z) ? na : z <= 0 ? color.gradient(t, 0.15, 1, color.slate.900, color.lime.300) : color.gradient(t, 0.15, 1, color.slate.900, color.fuchsia.400)
}
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
trending = 0
reverting = 0
best = 0
best_z = 0.0
for k in 0..=23 {
n = math.max(8, math.round(first * math.pow(ratio, k)))
(z, runs) = hft.runs_test(src, n)
if z <= -2 { trending := trending + 1 }
if z >= 2 { reverting := reverting + 1 }
if not math.is_na(z) and math.abs(z) > math.abs(best_z) {
best_z := z
best := n
}
top = render.line(k + 0.5, str.format("Edge {}", k + 1), color = color.transparent, width = 0)
render.band(top, edge, color = heat(z), opacity = 1, title = str.format("Row {}", k + 1))
edge := top
}
vr = hft.variance_ratio(src, vr_len, 4)
render.line(11.5 + math.clamp((vr - 1) * 20, -11.5, 11.5), "Variance ratio", color = color.translucent(color.white, 20), width = 1.6)
regime_trend = trending >= 12
regime_mr = reverting >= 12
render.mark(regime_trend and not regime_trend[1], shape = shape.triangle_up, at = anchor.bottom, color = color.lime.300, title = "Momentum regime")
render.mark(regime_mr and not regime_mr[1], shape = shape.diamond, at = anchor.top, color = color.fuchsia.400, title = "Mean-reversion regime")
persist t = draw.table.create(anchor.top_right, 3, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Trending horizons", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{} / 24", trending))
draw.table.cell(t, 1, 0, "Reverting horizons", text_color = color.slate.400)
draw.table.cell(t, 1, 1, str.format("{} / 24", reverting))
draw.table.cell(t, 2, 0, "Least efficient", text_color = color.slate.400)
draw.table.cell(t, 2, 1, str.format("{} bars (z {:+.1f})", best, best_z), bg_color = heat(best_z))
}
alert.rule(regime_trend and not regime_trend[1], "Momentum regime", "Half of the horizons show clustered (trending) moves")
alert.rule(regime_mr and not regime_mr[1], "Mean-reversion regime", "Half of the horizons show alternating (mean-reverting) moves")Microstructure Monitor
analysis · presets/analysis/hft/microstructure.qs
Flow toxicity and liquidity in one horizontal pane: VPIN (probability of informed trading), the net order-flow imbalance, and Kyle's lambda rescaled to its recent range. Background turns red when VPIN is in its top decile and the market is illiquid.
analysis "Microstructure Monitor" {
pane = hpane
height = 0.3
}
/// Flow toxicity and liquidity in one horizontal pane: VPIN (probability of informed trading),
/// the net order-flow imbalance, and Kyle's lambda rescaled to its recent range. Background turns
/// red when VPIN is in its top decile and the market is illiquid.
buckets = param.int(50, title = "VPIN buckets", min = 10, max = 200)
win = param.int(50, title = "Window", min = 10, max = 500)
vpin = hft.vpin(buckets)
(buy, sell, imb) = hft.flow(win)
lam = hft.kyle_lambda(win)
lam_hi = ta.highest(lam, 500)
lam_lo = ta.lowest(lam, 500)
lam_n = lam_hi > lam_lo ? (lam - lam_lo) / (lam_hi - lam_lo) : na
toxic = vpin > stat.percentile(vpin, 500, 90) and lam_n > 0.7
render.line(vpin, "VPIN", color = color.fuchsia.400, width = 2)
render.histogram(imb, "Flow imbalance", color = imb >= 0 ? color.emerald.500 : color.rose.500)
render.line(lam_n, "Kyle lambda (scaled)", color = color.sky.400)
render.level(0, "Zero", color = color.slate.600, stroke = style.dotted)
render.background(toxic ? color.translucent(color.red.500, 85) : na)
alert.rule(toxic and not toxic[1], "Toxic flow", "VPIN in its top decile with low liquidity")Order Flow Toxicity Field
analysis · presets/analysis/hft/toxicity_field.qs
VPIN (volume-synchronised probability of informed trading) on twelve horizons, from 10 to 120 volume buckets, as a heatmap. Each row is colored by its own percentile, so the field shows where order flow is unusually one-sided right now and how far up the horizons that toxicity has spread: a short-horizon flare is a burst of aggressive flow, a field that lights up all the way to the top is sustained informed trading, the classic precursor of liquidity withdrawal and gap moves. The line is Kyle's lambda (price impact per unit of signed volume) as a percentile: impact rising together with toxicity is the dangerous combination and is marked.
analysis "Order Flow Toxicity Field" {
pane = hpane
height = 0.28
render = webgpu
}
/// VPIN (volume-synchronised probability of informed trading) on twelve horizons, from 10 to
/// 120 volume buckets, as a heatmap. Each row is colored by its own percentile, so the field
/// shows where order flow is unusually one-sided right now and how far up the horizons that
/// toxicity has spread: a short-horizon flare is a burst of aggressive flow, a field that lights
/// up all the way to the top is sustained informed trading, the classic precursor of liquidity
/// withdrawal and gap moves.
/// The line is Kyle's lambda (price impact per unit of signed volume) as a percentile: impact
/// rising together with toxicity is the dangerous combination and is marked.
bucket_vol = param.float(0, title = "Bucket volume (0 = auto)", min = 0, group = "VPIN")
warm = param.int(50, title = "Warm-up bars", min = 5, max = 2000, group = "VPIN")
history = param.int(200, title = "Percentile history", min = 50, max = 5000, group = "Field")
lam_len = param.int(50, title = "Kyle lambda window", min = 10, max = 1000, group = "Impact")
fn heat(p: float) -> color {
return math.is_na(p) ? na : p >= 50 ? color.gradient(p, 50, 100, color.slate.900, color.orange.400) : color.gradient(p, 0, 50, color.indigo.900, color.slate.900)
}
edge = render.line(-0.5, "Edge 0", color = color.transparent, width = 0)
hot = 0
avg = 0.0
for k in 0..=11 {
v = hft.vpin(10 + k * 10, bucket_vol, warm)
p = stat.percent_rank(v, history)
if p >= 85 { hot := hot + 1 }
avg := avg + (math.is_na(p) ? 50 : p) / 12
top = render.line(k + 0.5, str.format("Edge {}", k + 1), color = color.transparent, width = 0)
render.band(top, edge, color = heat(p), opacity = 1, title = str.format("Row {}", k + 1))
edge := top
}
lam = hft.kyle_lambda(lam_len)
lam_p = stat.percent_rank(lam, history)
render.line(5.5 + (lam_p - 50) / 50 * 5.5, "Price impact", color = color.translucent(color.cyan.200, 15), width = 1.6)
cap_lo = render.line(12.1, "Consensus low", color = color.transparent, width = 0)
cap_hi = render.line(13.6, "Consensus high", color = color.transparent, width = 0)
render.band(cap_hi, cap_lo, color = heat(avg), opacity = 1, title = "Toxicity consensus")
spread = hot >= 8
danger = spread and lam_p >= 80
render.mark(spread and not spread[1], shape = shape.diamond, at = anchor.top, color = color.orange.300, title = "Toxicity spreading")
render.mark(danger and not danger[1], shape = shape.triangle_down, at = anchor.top, color = color.rose.400, title = "Toxic flow with high impact")
render.mark(hot == 0 and hot[1] > 3, shape = shape.circle, at = anchor.bottom, color = color.sky.300, size = 0.6, title = "Flow normalised")
persist t = draw.table.create(anchor.top_right, 3, 2)
if bar.is_last {
draw.table.cell(t, 0, 0, "Toxic horizons", text_color = color.slate.400)
draw.table.cell(t, 0, 1, str.format("{} / 12", hot), bg_color = heat(hot / 12 * 100))
draw.table.cell(t, 1, 0, "Toxicity pct", text_color = color.slate.400)
draw.table.cell(t, 1, 1, str.format("{:.0f}", avg), bg_color = heat(avg))
draw.table.cell(t, 2, 0, "Impact pct", text_color = color.slate.400)
draw.table.cell(t, 2, 1, math.is_na(lam_p) ? "-" : str.format("{:.0f}", lam_p))
}
alert.rule(danger and not danger[1], "Toxic flow", "VPIN is extreme on most horizons while price impact is in its top quintile")