Appearance

Personalize your experience

Mode
Accent Color
Direction
All Docs/QuaScript//
QuaScript logo

QuaScript Documentation

Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1

Statistics

Hurst Regime

analysis · presets/analysis/stats/hurst_regime.qs

Hurst exponent (rescaled range), fractal dimension and lag-1 autocorrelation of returns: three views of persistence. H > 0.5 trends, H < 0.5 mean-reverts.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
hurst_regime.qs
analysis "Hurst Regime" {
    overlay = false
}
/// Hurst exponent (rescaled range), fractal dimension and lag-1 autocorrelation of returns: three
/// views of persistence. H > 0.5 trends, H < 0.5 mean-reverts.
len = param.int(100, title = "Window", min = 20, max = 1000)

ret = ta.change(close) / close[1]
h = stat.hurst(close, len)
fd = stat.fractal_dim(close, len)
ac = stat.autocorr(ret, len, 1)
render.line(h, "Hurst", color = h > 0.55 ? color.emerald.400 : h < 0.45 ? color.rose.400 : color.slate.400, width = 2)
render.line(2 - fd, "2 - fractal dim", color = color.violet.400, width = 1)
render.histogram(0.5 + ac, "0.5 + autocorr", base = 0.5, color = ac >= 0 ? color.translucent(color.emerald.500, 60) : color.translucent(color.rose.500, 60))
render.level(0.5, "Random walk", color = color.slate.500, stroke = style.dashed)
render.level(0.55, "Trend", color = color.emerald.800, stroke = style.dotted)
render.level(0.45, "Revert", color = color.rose.800, stroke = style.dotted)

Kalman Spread Z-Score

analysis · presets/analysis/stats/kalman_spread.qs

A dynamic hedge between price and its own slow trend, estimated with a Kalman filter: the time-varying beta, the residual spread and its z-score. The z-score is drawn as a gradient area with stretched zones; marks show the spread snapping back toward zero from an extreme.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
kalman_spread.qs
analysis "Kalman Spread Z-Score" {
    pane   = hpane
    height = 0.24
}
/// A dynamic hedge between price and its own slow trend, estimated with a Kalman filter: the
/// time-varying beta, the residual spread and its z-score. The z-score is drawn as a gradient
/// area with stretched zones; marks show the spread snapping back toward zero from an extreme.
trend_len = param.int(50, title = "Trend length", min = 5, max = 500)
delta     = param.float(0.0001, title = "Adaptation", min = 0.000001, max = 0.01, step = 0.00001)
entry     = param.float(1.5, title = "Entry z", min = 0.5, max = 5, step = 0.1)

(beta, alpha, spread, z) = hft.kalman_beta(close, ta.ema(close, trend_len), delta, 0.001)
render.area(z, "Spread z-score", base = 0, color = z >= 0 ? color.translucent(color.fuchsia.400, 55) : color.translucent(color.cyan.400, 55))
render.line(ta.ema(z, 5), "Smoothed z", color = color.white, width = 1.5)
render.line((beta - 1) * 20, "Beta deviation (x20)", color = color.translucent(color.amber.300, 30), width = 1, stroke = style.dashed)
top = render.level(3, "Top", color = color.transparent)
hi = render.level(entry, "Rich", color = color.fuchsia.700, stroke = style.dashed)
lo = render.level(-entry, "Cheap", color = color.cyan.700, stroke = style.dashed)
bot = render.level(-3, "Bottom", color = color.transparent)
render.band(top, hi, color = color.fuchsia.500, opacity = 0.08, title = "Rich zone")
render.band(lo, bot, color = color.cyan.500, opacity = 0.08, title = "Cheap zone")
render.level(0, "Fair", color = color.slate.600)
snap_dn = z[1] > entry and z < entry
snap_up = z[1] < -entry and z > -entry
render.mark(snap_dn, shape = shape.triangle_down, at = anchor.top, color = color.fuchsia.400, title = "Rich snap-back")
render.mark(snap_up, shape = shape.triangle_up, at = anchor.bottom, color = color.cyan.400, title = "Cheap snap-back")
alert.rule(snap_dn or snap_up, "Spread snap-back", "The Kalman spread left an extreme")

Regression Channel

analysis · presets/analysis/stats/linreg_channel.qs

Least-squares regression line of the last N bars, redrawn on the last bar as one segment with parallel channel lines at the maximum residual and +-2 standard errors. R-squared and slope are shown in a table; a developing regression value is plotted on every bar.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
linreg_channel.qs
analysis "Regression Channel" {
    overlay     = true
    max_objects = 10
}
/// Least-squares regression line of the last N bars, redrawn on the last bar as one segment with
/// parallel channel lines at the maximum residual and +-2 standard errors. R-squared and slope
/// are shown in a table; a developing regression value is plotted on every bar.
len = param.int(100, title = "Window", min = 10, max = 2000)

(value, slope, intercept) = stat.linreg(close, len, 0)
r2 = stat.r_squared(close, len)
resid_sd = stat.stdev(close - value, len)
render.line(value, "Developing regression", color = color.sky.400, width = 1, stroke = style.dotted)

persist mid_id = 0.0
persist up_id = 0.0
persist dn_id = 0.0
persist tbl = draw.table.create(anchor.bottom_right, 3, 2)
if bar.is_last {
    x0 = bar.index - len + 1
    y0 = value - slope * (len - 1)
    if mid_id == 0 {
        mid_id := draw.segment.create(x0, y0, bar.index, value, color = color.sky.400, width = 2, ray = ray.right)
        up_id := draw.segment.create(x0, y0 + 2 * resid_sd, bar.index, value + 2 * resid_sd, color = color.rose.400, ray = ray.right)
        dn_id := draw.segment.create(x0, y0 - 2 * resid_sd, bar.index, value - 2 * resid_sd, color = color.teal.400, ray = ray.right)
    } else {
        draw.segment.update(mid_id, x0, y0, bar.index, value)
        draw.segment.update(up_id, x0, y0 + 2 * resid_sd, bar.index, value + 2 * resid_sd)
        draw.segment.update(dn_id, x0, y0 - 2 * resid_sd, bar.index, value - 2 * resid_sd)
    }
    draw.table.cell(tbl, 0, 0, "Slope / bar")
    draw.table.cell(tbl, 0, 1, str.format("{:.4f}", slope))
    draw.table.cell(tbl, 1, 0, "R squared")
    draw.table.cell(tbl, 1, 1, str.format("{:.3f}", r2), bg_color = color.gradient(r2, 0, 1, color.slate.800, color.emerald.700))
    draw.table.cell(tbl, 2, 0, "Std error")
    draw.table.cell(tbl, 2, 1, str.format("{:.2f}", resid_sd))
}

Local Linear Kernel Smoother

analysis · presets/analysis/stats/local_linear_smoother.qs

A local linear regression of price on time with Gaussian weights, fitted at the newest bar only (no future bars, so history never changes). Unlike a kernel-weighted average, the local linear fit removes the lag of a trending series at the right edge. The fitted slope colors the line; bands are drawn at a multiple of the weighted residual standard deviation of the same fit, with gradient fills. Touches of the outer band against the slope are marked as stretched, and slope sign changes as turns. Method: Cleveland (1979), Fan and Gijbels (1996).

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
local_linear_smoother.qs
analysis "Local Linear Kernel Smoother" {
    overlay = true
}
/// A local linear regression of price on time with Gaussian weights, fitted at the newest bar
/// only (no future bars, so history never changes). Unlike a kernel-weighted average, the local
/// linear fit removes the lag of a trending series at the right edge. The fitted slope colors
/// the line; bands are drawn at a multiple of the weighted residual standard deviation of the
/// same fit, with gradient fills. Touches of the outer band against the slope are marked as
/// stretched, and slope sign changes as turns. Method: Cleveland (1979), Fan and Gijbels (1996).
src   = param.source(close, title = "Source")
look  = param.int(80, title = "Window bars", min = 10, max = 400)
bw    = param.float(14, title = "Bandwidth (bars)", min = 2, max = 200, step = 0.5)
k_in  = param.float(1.0, title = "Inner band (sigma)", min = 0.25, max = 4, step = 0.05)
k_out = param.float(2.2, title = "Outer band (sigma)", min = 0.5, max = 6, step = 0.05)
// weighted least squares at x = 0 (the newest bar), x = -i for i bars back
sw = 0.0
swx = 0.0
swxx = 0.0
swy = 0.0
swxy = 0.0
for i in 0..look {
    y = src[i]
    if not math.is_na(y) {
        w = math.exp(-0.5 * (i * i) / (bw * bw))
        x = -i
        sw := sw + w
        swx := swx + w * x
        swxx := swxx + w * x * x
        swy := swy + w * y
        swxy := swxy + w * x * y
    }
}
det = sw * swxx - swx * swx
fit = det != 0 ? (swxx * swy - swx * swxy) / det : na
slope = det != 0 ? (sw * swxy - swx * swy) / det : na
// weighted residual spread of the same local fit
sr = 0.0
for i in 0..look {
    y = src[i]
    if not math.is_na(y) and not math.is_na(fit) {
        w = math.exp(-0.5 * (i * i) / (bw * bw))
        e = y - (fit - slope * i)
        sr := sr + w * e * e
    }
}
sigma = sw > 0 ? math.sqrt(sr / sw) : na
up = slope > 0
p_fit = render.line(fit, "Local fit", color = up ? color.sky.300 : color.amber.300, width = 3)
p_hi = render.line(fit + k_out * sigma, "Upper outer", color = color.translucent(color.amber.300, 45), width = 1)
p_hi2 = render.line(fit + k_in * sigma, "Upper inner", color = color.transparent, width = 0)
p_lo2 = render.line(fit - k_in * sigma, "Lower inner", color = color.transparent, width = 0)
p_lo = render.line(fit - k_out * sigma, "Lower outer", color = color.translucent(color.sky.300, 45), width = 1)
render.band(p_hi, p_hi2, texture = "gradient", upper_color = color.amber.500, lower_color = color.slate.900, opacity = 0.32, title = "Upper stretch")
render.band(p_hi2, p_lo2, color = up ? color.sky.500 : color.amber.500, opacity = 0.06, title = "Fair range")
render.band(p_lo2, p_lo, texture = "gradient", upper_color = color.slate.900, lower_color = color.sky.500, opacity = 0.32, title = "Lower stretch")
stretched_up = high > fit + k_out * sigma and not up
stretched_dn = low < fit - k_out * sigma and up
turn_up = up and not up[1]
turn_dn = not up and up[1]
render.mark(stretched_up, shape = shape.circle, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Stretched high")
render.mark(stretched_dn, shape = shape.circle, at = anchor.below_bar, color = color.sky.300, size = 0.7, title = "Stretched low")
render.mark(turn_up, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, text = "Turn")
render.mark(turn_dn, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, text = "Turn")
alert.rule(turn_up or turn_dn, "Smoother turn", "The local linear slope changed sign")
alert.rule(stretched_up or stretched_dn, "Stretched against trend", "Price reached the outer band against the fitted slope")

Ornstein-Uhlenbeck Reversion

analysis · presets/analysis/stats/ou_reversion_bands.qs

Fits an Ornstein-Uhlenbeck process to price: the long-run mean, the half-life of reversion and the z-score of the distance from the mean. Bands at one, two and three sigma are gradient filled, the mean is shaded by how fast the market reverts, and reversion entries are marked when an extreme z-score turns back with a short half-life.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
ou_reversion_bands.qs
analysis "Ornstein-Uhlenbeck Reversion" {
    overlay = true
}
/// Fits an Ornstein-Uhlenbeck process to price: the long-run mean, the half-life of reversion and
/// the z-score of the distance from the mean. Bands at one, two and three sigma are gradient
/// filled, the mean is shaded by how fast the market reverts, and reversion entries are marked
/// when an extreme z-score turns back with a short half-life.
len = param.int(120, title = "Fit length", min = 30, max = 1000)
hl_max = param.float(40, title = "Max half-life (bars)", min = 2, max = 500, step = 1)

(mu_fit, hl, z_fit) = hft.ou(close, len)
// a fit that drifts away from price (no reversion in the window) falls back to the rolling mean
base = ta.sma(close, len)
ok = not math.is_na(mu_fit) and math.abs(mu_fit - base) < 3 * ta.atr(len)
mu = ok ? mu_fit : base
sd = stat.stdev(close - mu, len)
z = ok ? z_fit : (close - mu) / math.max(sd, 1e-12)
fast = ok and hl > 0 and hl < hl_max
tone = fast ? color.gradient(hl, 0, hl_max, color.cyan.300, color.violet.400) : color.slate.400
render.line(mu, "OU mean", color = tone, width = 3)
u1 = render.line(mu + sd, "+1 sigma", color = color.translucent(color.violet.300, 60), width = 1)
d1 = render.line(mu - sd, "-1 sigma", color = color.translucent(color.violet.300, 60), width = 1)
u2 = render.line(mu + 2 * sd, "+2 sigma", color = color.translucent(color.fuchsia.300, 45), width = 1)
d2 = render.line(mu - 2 * sd, "-2 sigma", color = color.translucent(color.cyan.300, 45), width = 1)
u3 = render.line(mu + 3 * sd, "+3 sigma", color = color.translucent(color.fuchsia.400, 30), width = 1.5)
d3 = render.line(mu - 3 * sd, "-3 sigma", color = color.translucent(color.cyan.400, 30), width = 1.5)
render.band(u1, d1, color = color.violet.500, opacity = 0.06, title = "Fair value")
render.band(u3, u2, texture = "gradient", upper_color = color.fuchsia.500, lower_color = color.violet.900, opacity = 0.25, title = "Rich")
render.band(d2, d3, texture = "gradient", upper_color = color.violet.900, lower_color = color.cyan.500, opacity = 0.25, title = "Cheap")
buy = fast and z[1] < -2 and z > z[1]
sell = fast and z[1] > 2 and z < z[1]
render.mark(buy, shape = shape.tag_up, at = anchor.below_bar, color = color.cyan.500, text = "Revert")
render.mark(sell, shape = shape.tag_down, at = anchor.above_bar, color = color.fuchsia.500, text = "Revert")
render.candle_color(z > 2 ? color.fuchsia.400 : z < -2 ? color.cyan.400 : na)
alert.rule(buy, "Reversion up", "Price turned up from two sigma below the OU mean")
alert.rule(sell, "Reversion down", "Price turned down from two sigma above the OU mean")

Regime Entropy Map

analysis · presets/analysis/stats/regime_entropy_map.qs

Classifies the market into four regimes from the Hurst exponent, the Shannon entropy of returns and Kaufman efficiency: trending, mean-reverting, random and chaotic. The regime score is a colored column histogram, the three inputs are drawn as lines, the pane background takes the regime color and a table explains the current reading.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
regime_entropy_map.qs
analysis "Regime Entropy Map" {
    pane   = hpane
    height = 0.25
}
/// Classifies the market into four regimes from the Hurst exponent, the Shannon entropy of
/// returns and Kaufman efficiency: trending, mean-reverting, random and chaotic. The regime score
/// is a colored column histogram, the three inputs are drawn as lines, the pane background takes
/// the regime color and a table explains the current reading.
len  = param.int(100, title = "Hurst length", min = 30, max = 500)
elen = param.int(50, title = "Entropy length", min = 10, max = 300)
bins = param.int(10, title = "Entropy bins", min = 4, max = 30)

ret = math.log(close / close[1])
h = stat.hurst(close, len)
ent = stat.entropy(ret, elen, bins) / math.log(bins)
er = math.abs(close - close[elen]) / math.max(stat.sum(math.abs(close - close[1]), elen), 1e-12)
// +1 trending .. -1 mean-reverting; chaotic when entropy is near its maximum
score = math.clamp((h - 0.5) * 4 + (er - 0.3) * 2, -1, 1)
chaotic = ent > 0.92 and er < 0.15
regime = chaotic ? 3 : score > 0.25 ? 1 : score < -0.25 ? 2 : 0
rc = match regime {
    case 1 { color.emerald.500 }
    case 2 { color.violet.500 }
    case 3 { color.orange.500 }
    else { color.slate.500 }
}
render.histogram(score, "Regime score", color = color.translucent(rc, 25))
render.line(h * 2 - 1, "Hurst (scaled)", color = color.sky.300, width = 1.5)
render.line(ent * 2 - 1, "Entropy (scaled)", color = color.orange.300, width = 1, stroke = style.dashed)
render.spline(er * 2 - 1, "Efficiency (scaled)", color = color.lime.300, width = 1)
render.level(0, "Random walk", color = color.slate.600, stroke = style.dotted)
render.background(color.translucent(rc, 90))
render.mark(regime == 1 and regime[1] != 1, shape = shape.circle, at = anchor.top, color = color.emerald.400, size = 0.6, title = "Trend regime")
render.mark(regime == 2 and regime[1] != 2, shape = shape.circle, at = anchor.bottom, color = color.violet.400, size = 0.6, title = "Reversion regime")
persist t = draw.table.create(anchor.top_left, 4, 2)
if bar.is_last {
    name = match regime {
        case 1 { "Trending" }
        case 2 { "Mean-reverting" }
        case 3 { "Chaotic" }
        else { "Random" }
    }
    draw.table.cell(t, 0, 0, "Regime", text_color = color.slate.400)
    draw.table.cell(t, 0, 1, name, bg_color = rc, text_color = color.readable(rc))
    draw.table.cell(t, 1, 0, "Hurst", text_color = color.slate.400)
    draw.table.cell(t, 1, 1, str.format("{:.2f}", h))
    draw.table.cell(t, 2, 0, "Entropy", text_color = color.slate.400)
    draw.table.cell(t, 2, 1, str.format("{:.0f}%", ent * 100))
    draw.table.cell(t, 3, 0, "Efficiency", text_color = color.slate.400)
    draw.table.cell(t, 3, 1, str.format("{:.0f}%", er * 100))
}
alert.rule(regime == 1 and regime[1] != 1, "Trend regime", "Hurst and efficiency point to a persistent trend")
alert.rule(regime == 3 and regime[1] != 3, "Chaotic regime", "Returns are close to maximum entropy")

Regression Fan

analysis · presets/analysis/stats/regression_fan.qs

Linear-regression channels over three horizons drawn from the newest bar back: each channel is a centre line with two-sigma edges, projected forward, labelled with its slope and R². The rolling regression value is plotted for the middle horizon so history stays readable.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
regression_fan.qs
analysis "Regression Fan" {
    overlay     = true
    max_objects = 60
}
/// Linear-regression channels over three horizons drawn from the newest bar back: each channel
/// is a centre line with two-sigma edges, projected forward, labelled with its slope and R². The
/// rolling regression value is plotted for the middle horizon so history stays readable.
short = param.int(50, title = "Short horizon", min = 10, max = 500)
mid   = param.int(120, title = "Middle horizon", min = 20, max = 1000)
long  = param.int(250, title = "Long horizon", min = 30, max = 2000)
ext   = param.int(15, title = "Projection bars", min = 0, max = 100)

(v1, s1, i1) = stat.linreg(close, short, 0)
(v2, s2, i2) = stat.linreg(close, mid, 0)
(v3, s3, i3) = stat.linreg(close, long, 0)
d1 = stat.stdev(close - v1, short)
d2 = stat.stdev(close - v2, mid)
d3 = stat.stdev(close - v3, long)
r1 = stat.r_squared(close, short)
r2 = stat.r_squared(close, mid)
r3 = stat.r_squared(close, long)
render.line(v2, "Regression value", color = color.translucent(color.sky.300, 40), width = 1)
persist ids = list.create(float)
persist tags = list.create(float)
fn channel(n: int, v: float, slope: float, sd: float, r2v: float, tone: color, ids: list<float>, tags: list<float>, ext: int) {
    x0 = bar.index - n + 1
    y0 = v - slope * (n - 1)
    x1 = bar.index + ext
    y1 = v + slope * ext
    list.push(ids, draw.segment.create(x0, y0, x1, y1, color = tone, width = 2))
    list.push(ids, draw.segment.create(x0, y0 + 2 * sd, x1, y1 + 2 * sd, color = color.translucent(tone, 40), stroke = style.dashed))
    list.push(ids, draw.segment.create(x0, y0 - 2 * sd, x1, y1 - 2 * sd, color = color.translucent(tone, 40), stroke = style.dashed))
    list.push(tags, draw.tag.create(x1, y1, str.format("{} bars  slope {:.3f}  R² {:.2f}", n, slope, r2v), color = color.translucent(tone, 20), shape = shape.tag, size = 0.8))
}
if bar.is_last {
    // the live bar re-runs: replace the previous channels
    for id in ids { draw.segment.delete(id) }
    for id in tags { draw.tag.delete(id) }
    list.clear(ids)
    list.clear(tags)
    channel(long, v3, s3, d3, r3, color.violet.400, ids, tags, ext)
    channel(mid, v2, s2, d2, r2, color.sky.400, ids, tags, ext)
    channel(short, v1, s1, d1, r1, color.amber.400, ids, tags, ext)
}

Structural Break Detector

analysis · presets/analysis/stats/structural_breaks.qs

Detects regime changes with three statistics: a CUSUM test on price (breaks in the mean), the variance ratio (above one trends, below one mean-reverts) and bipower variation (jumps that continuous volatility does not explain). Breaks are drawn as time marks across the pane and jump bars are highlighted.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
structural_breaks.qs
analysis "Structural Break Detector" {
    pane   = hpane
    height = 0.22
}
/// Detects regime changes with three statistics: a CUSUM test on price (breaks in the mean), the
/// variance ratio (above one trends, below one mean-reverts) and bipower variation (jumps that
/// continuous volatility does not explain). Breaks are drawn as time marks across the pane and
/// jump bars are highlighted.
len  = param.int(50, title = "Window", min = 10, max = 500)
mult = param.float(4.0, title = "CUSUM threshold (sigma)", min = 0.5, max = 6, step = 0.1)

brk = hft.cusum(close, 0, len, mult)
vr = hft.variance_ratio(close, len * 2, 4)
(rv, bv, jump) = hft.bipower(close, len)
render.histogram(vr - 1, "Variance ratio - 1", color = vr >= 1 ? color.translucent(color.emerald.500, 35) : color.translucent(color.violet.500, 35))
render.line(jump * 10, "Jump share (x10)", color = color.orange.400, width = 1.5)
render.spline(ta.ema(vr - 1, 10), "Trend persistence", color = color.sky.300, width = 2)
render.level(0, "Random walk", color = color.slate.600, stroke = style.dotted)
render.time_mark(brk != 0, title = "Structural break", color = color.amber.400, width = 1, stroke = style.dashed)
render.mark(brk > 0, shape = shape.triangle_up, at = anchor.bottom, color = color.emerald.400, title = "Break up")
render.mark(brk < 0, shape = shape.triangle_down, at = anchor.top, color = color.rose.400, title = "Break down")
render.background(jump > 0.3 ? color.translucent(color.orange.500, 85) : na)
alert.rule(brk != 0, "Structural break", "CUSUM detected a shift in the mean")

Variance Ratio Regime

analysis · presets/analysis/stats/variance_ratio_regime.qs

Lo-MacKinlay variance ratios for holding periods 2, 4 and 8. A ratio above one means returns trend (they add up faster than a random walk), below one means they mean-revert. The three lines and their average decide the regime shaded in the background, useful as a switch between trend and reversion tactics.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
variance_ratio_regime.qs
analysis "Variance Ratio Regime" {
    pane   = hpane
    height = 0.2
    render = webgpu
}
/// Lo-MacKinlay variance ratios for holding periods 2, 4 and 8. A ratio above one means returns
/// trend (they add up faster than a random walk), below one means they mean-revert. The three
/// lines and their average decide the regime shaded in the background, useful as a switch between
/// trend and reversion tactics.
len = param.int(200, title = "Window", min = 30, max = 5000)
v2 = hft.variance_ratio(close, len, 2)
v4 = hft.variance_ratio(close, len, 4)
v8 = hft.variance_ratio(close, len, 8)
avg = (v2 + v4 + v8) / 3
render.line(v2, "VR(2)", color = color.translucent(color.sky.300, 30), width = 1)
render.line(v4, "VR(4)", color = color.translucent(color.violet.300, 30), width = 1)
render.line(v8, "VR(8)", color = color.translucent(color.fuchsia.300, 30), width = 1)
render.line(avg, "Average", color = color.white, width = 2)
render.level(1, "Random walk", color = color.translucent(color.slate.400, 50))
render.background(avg > 1.1 ? color.translucent(color.emerald.500, 88) : avg < 0.9 ? color.translucent(color.amber.500, 88) : na)

Z-Score Bands

analysis · presets/analysis/stats/zscore_bands.qs · uses viz/palette

Mean-reversion envelope: the rolling mean plus 1 / 2 / 3 standard deviations, candles colored by the current z-score, and marks when price snaps back inside the 2-sigma band.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
zscore_bands.qs
analysis "Z-Score Bands" {
    overlay = true
}
/// Mean-reversion envelope: the rolling mean plus 1 / 2 / 3 standard deviations, candles colored
/// by the current z-score, and marks when price snaps back inside the 2-sigma band.
use "viz/palette" as pal

len = param.int(50, title = "Window", min = 5, max = 1000)

m = ta.sma(close, len)
sd = stat.stdev(close, len)
z = sd > 0 ? (close - m) / sd : 0
render.line(m, "Mean", color = color.slate.300, width = 1)
for k in [1, 2, 3] {
    render.line(m + k * sd, str.format("+{} sd", k), color = k == 3 ? color.rose.500 : color.slate.600, width = 1, stroke = k == 1 ? style.dotted : style.solid)
    render.line(m - k * sd, str.format("-{} sd", k), color = k == 3 ? color.teal.500 : color.slate.600, width = 1, stroke = k == 1 ? style.dotted : style.solid)
}
render.candle_color(pal.signed_heat(z, 3))
render.mark(ta.cross_down(z, 2), shape = shape.triangle_down, at = anchor.above_bar, color = pal.BEAR, text = "Revert")
render.mark(ta.cross_up(z, -2), shape = shape.triangle_up, at = anchor.below_bar, color = pal.BULL, text = "Revert")

Z-Score Heatmap

analysis · presets/analysis/stats/zscore_heatmap.qs

The z-score of price against its mean over twelve lookbacks, one heatmap row each: blue when price is cheap, red when rich. Reading a column from short (bottom) to long (top) separates a short-term stretch from a long-term extreme. The top row is the average.

QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
zscore_heatmap.qs
analysis "Z-Score Heatmap" {
    pane   = hpane
    height = 0.24
}
/// The z-score of price against its mean over twelve lookbacks, one heatmap row each: blue when
/// price is cheap, red when rich. Reading a column from short (bottom) to long (top) separates a
/// short-term stretch from a long-term extreme. The top row is the average.
gap   = param.int(20, title = "Lookback step", min = 2, max = 100)
first = param.int(10, title = "Shortest lookback", min = 5, max = 200)

total = 0.0
for k in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] {
    z = stat.zscore(close, first + k * gap)
    total := total + z
    c = z >= 0 ? color.gradient(z, 0, 2.5, color.slate.800, color.rose.400) : color.gradient(z, -2.5, 0, color.sky.400, color.slate.800)
    render.line(k, str.format("Row {}", k + 1), color = math.abs(z) > 3 ? color.yellow.300 : c, width = 10)
}
avg = total / 12
avg_c = avg >= 0 ? color.gradient(avg, 0, 2, color.slate.700, color.rose.500) : color.gradient(avg, -2, 0, color.sky.500, color.slate.700)
render.line(13, "Average z", color = avg_c, width = 13)
render.mark(avg > 2 and avg[1] <= 2, shape = shape.diamond, at = anchor.top, color = color.rose.400, title = "Rich on every horizon")
render.mark(avg < -2 and avg[1] >= -2, shape = shape.diamond, at = anchor.bottom, color = color.sky.400, title = "Cheap on every horizon")
alert.rule(avg > 2 and avg[1] <= 2, "Rich", "Average z-score across twelve lookbacks above 2")
alert.rule(avg < -2 and avg[1] >= -2, "Cheap", "Average z-score across twelve lookbacks below -2")

© 2026 Quatick Technologies. All rights reserved.