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

Trendlines and levels

Adaptive Regression Channel

analysis · presets/analysis/levels/adaptive_regression_channel.qs · uses viz/premium

A linear regression channel that picks its own length. Twelve candidate windows (from the shortest to the longest, spaced geometrically) are fitted on every bar and the one with the strongest Pearson correlation between price and time wins, so the channel always describes the cleanest current trend instead of a fixed number of bars. The winning channel is drawn from its first bar to the newest, with 1 and 2 sigma bands of the residuals, projected forward. Reading: high |r| (above 0.8) = a clean trend you can trade towards the opposite band; a sudden drop in the chosen length = the old trend broke and a new one is forming. The history line is the end point of the winning fit, coloured by slope. Signals: closes beyond 2 sigma in the direction of the slope (expansion) and against it (exhaustion). Method: least squares, Pearson r, residual standard deviation.

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)
adaptive_regression_channel.qs
analysis "Adaptive Regression Channel" {
    overlay     = true
    render      = webgpu
    short_title = "ARC"
    max_objects = 40
}
/// A linear regression channel that picks its own length. Twelve candidate windows (from the
/// shortest to the longest, spaced geometrically) are fitted on every bar and the one with the
/// strongest Pearson correlation between price and time wins, so the channel always describes the
/// cleanest current trend instead of a fixed number of bars. The winning channel is drawn from its
/// first bar to the newest, with 1 and 2 sigma bands of the residuals, projected forward.
/// Reading: high |r| (above 0.8) = a clean trend you can trade towards the opposite band; a
/// sudden drop in the chosen length = the old trend broke and a new one is forming. The history
/// line is the end point of the winning fit, coloured by slope. Signals: closes beyond 2 sigma in
/// the direction of the slope (expansion) and against it (exhaustion).
/// Method: least squares, Pearson r, residual standard deviation.
use "viz/premium" as pr
l_min = param.int(30, title = "Shortest window", min = 10, max = 500, group = "Search")
l_max = param.int(400, title = "Longest window", min = 50, max = 3000, group = "Search")
k1    = param.float(1.0, title = "Inner band (sigma)", min = 0.25, max = 4, step = 0.05, group = "Bands")
k2    = param.float(2.0, title = "Outer band (sigma)", min = 0.5, max = 6, step = 0.05, group = "Bands")
ext   = param.int(20, title = "Projection bars", min = 0, max = 200, group = "Bands")
ratio = math.pow(l_max / l_min, 1 / 11)
best_r = 0.0
best_len = l_min
best_v = na
best_s = na
best_sd = na
for k in 0..=11 {
    len = math.round(l_min * math.pow(ratio, k))
    (v, s, _) = stat.linreg(close, len, 0)
    r2 = stat.r_squared(close, len)
    sd = stat.stdev(close - v, len)
    if r2 > best_r {
        best_r := r2
        best_len := len
        best_v := v
        best_s := s
        best_sd := sd
    }
}
pearson = math.sign(best_s) * math.sqrt(math.max(best_r, 0))
up = best_s > 0
p_c = render.line(best_v, "Regression end", color = up ? color.sky.300 : color.amber.300, width = 2)
p_u2 = render.line(best_v + k2 * best_sd, "Upper 2 sigma", color = color.translucent(color.amber.300, 50), width = 1)
p_u1 = render.line(best_v + k1 * best_sd, "Upper 1 sigma", color = color.transparent, width = 0)
p_l1 = render.line(best_v - k1 * best_sd, "Lower 1 sigma", color = color.transparent, width = 0)
p_l2 = render.line(best_v - k2 * best_sd, "Lower 2 sigma", color = color.translucent(color.sky.300, 50), width = 1)
render.band(p_u2, p_u1, texture = "gradient", direction = "edges", upper_color = color.amber.500, lower_color = color.indigo.900, opacity = 0.26, title = "Upper zone")
render.band(p_u1, p_l1, color = up ? color.sky.500 : color.amber.500, opacity = 0.05, title = "Fair channel")
render.band(p_l1, p_l2, texture = "gradient", direction = "edges", upper_color = color.indigo.900, lower_color = color.sky.500, opacity = 0.26, title = "Lower zone")
expand = up ? close > best_v + k2 * best_sd : close < best_v - k2 * best_sd
exhaust = up ? close < best_v - k2 * best_sd : close > best_v + k2 * best_sd
render.mark(expand and not expand[1] and up, shape = shape.circle, at = anchor.above_bar, color = color.amber.300, size = 0.6, title = "Expansion up")
render.mark(expand and not expand[1] and not up, shape = shape.circle, at = anchor.below_bar, color = color.amber.300, size = 0.6, title = "Expansion down")
render.mark(exhaust and not exhaust[1], shape = shape.x, at = anchor.above_bar, color = color.rose.300, size = 0.7, title = "Channel break against trend")
alert.rule(exhaust and not exhaust[1], "Regression channel break", "Price closed beyond 2 sigma against the channel slope")
persist ids = list.create(id)
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last and not math.is_na(best_v) {
    for i in ids { draw.segment.delete(i) }
    list.clear(ids)
    // exact residual sigma of the winning window, from its own fitted line
    x0 = bar.index - best_len + 1
    y0 = best_v - best_s * (best_len - 1)
    ss = 0.0
    for i in 0..best_len {
        e = close[i] - (best_v - best_s * i)
        ss := ss + e * e
    }
    sd = math.sqrt(ss / best_len)
    x1 = bar.index + ext
    y1 = best_v + best_s * ext
    tone = up ? color.sky.300 : color.amber.300
    list.push(ids, draw.segment.create(x0, y0, x1, y1, color = tone, width = 2))
    for m in [-k2, -k1, k1, k2] {
        list.push(ids, draw.segment.create(x0, y0 + m * sd, x1, y1 + m * sd, color = color.translucent(tone, math.abs(m) == k2 ? 30 : 60), width = 1, stroke = math.abs(m) == k2 ? style.solid : style.dashed))
    }
    pr.heading(t, 2, "Adaptive Regression Channel", "Levels · best-fit window search")
    pr.row(t, 2, "Chosen window", str.format("{} bars", best_len), color.white)
    pr.row(t, 3, "Pearson r", str.format("{:+.3f}", pearson), math.abs(pearson) > 0.8 ? pr.BULL : color.slate.300)
    pr.row(t, 4, "Slope per bar", str.format("{:+.4f} ({:+.3%})", best_s, best_s / close), up ? pr.BULL : pr.BEAR)
    pr.row(t, 5, "Position", str.format("{:+.2f} sigma", (close - best_v) / math.max(sd, 1e-12)), color.white)
}

Anchored VWAP Constellation

analysis · presets/analysis/levels/anchored_vwap_constellation.qs · uses viz/premium

Eight volume-weighted average prices, each anchored at one of the last four confirmed swing lows (emerald family) and the last four swing highs (rose family). An anchored VWAP is the average price paid by everyone who traded since that swing, so it is where those buyers or sellers are break-even. Newer anchors are brighter; older ones fade. The cloud between the highest and lowest low-anchored lines is the buyers' cost band, coloured by how much of the constellation price is above. Reading: price above most lines = the recent crowd is in profit and tends to defend dips; price below most = trapped buyers become supply. A reclaim of the newest swing-high VWAP is the first sign that sellers since the last top are under water. Signals: reclaim of the newest swing-high VWAP, loss of the newest swing-low VWAP, full constellation support.

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)
anchored_vwap_constellation.qs
analysis "Anchored VWAP Constellation" {
    overlay     = true
    render      = webgpu
    short_title = "AVC"
    precision   = 2
}
/// Eight volume-weighted average prices, each anchored at one of the last four confirmed swing
/// lows (emerald family) and the last four swing highs (rose family). An anchored VWAP is the
/// average price paid by everyone who traded since that swing, so it is where those buyers or
/// sellers are break-even. Newer anchors are brighter; older ones fade. The cloud between the
/// highest and lowest low-anchored lines is the buyers' cost band, coloured by how much of the
/// constellation price is above.
/// Reading: price above most lines = the recent crowd is in profit and tends to defend dips;
/// price below most = trapped buyers become supply. A reclaim of the newest swing-high VWAP is
/// the first sign that sellers since the last top are under water. Signals: reclaim of the
/// newest swing-high VWAP, loss of the newest swing-low VWAP, full constellation support.
use "viz/premium" as pr
left  = param.int(8, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right = param.int(4, title = "Swing bars right", min = 1, max = 30, group = "Swings")
margin = param.float(0.3, title = "Cross margin (ATR)", min = 0, max = 3, step = 0.05, group = "Signals")
quiet = param.int(20, title = "Bars between repeats", min = 1, max = 500, group = "Signals")
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
tp = bar.typical
pv = tp * volume
persist l0p = na
persist l0v = na
persist l1p = na
persist l1v = na
persist l2p = na
persist l2v = na
persist l3p = na
persist l3v = na
persist h0p = na
persist h0v = na
persist h1p = na
persist h1v = na
persist h2p = na
persist h2v = na
persist h3p = na
persist h3v = na
// accumulate this bar first, then shift in a new anchor (which already includes this bar)
l0p := l0p + pv
l0v := l0v + volume
l1p := l1p + pv
l1v := l1v + volume
l2p := l2p + pv
l2v := l2v + volume
l3p := l3p + pv
l3v := l3v + volume
h0p := h0p + pv
h0v := h0v + volume
h1p := h1p + pv
h1v := h1v + volume
h2p := h2p + pv
h2v := h2v + volume
h3p := h3p + pv
h3v := h3v + volume
// price-volume and volume since the swing bar `right` bars ago, inclusive
sp = 0.0
sv = 0.0
if not math.is_na(pl) or not math.is_na(ph) {
    for i in 0..=right {
        sp := sp + pv[i]
        sv := sv + volume[i]
    }
}
if not math.is_na(pl) {
    l3p := l2p
    l3v := l2v
    l2p := l1p
    l2v := l1v
    l1p := l0p
    l1v := l0v
    l0p := sp
    l0v := sv
}
if not math.is_na(ph) {
    h3p := h2p
    h3v := h2v
    h2p := h1p
    h2v := h1v
    h1p := h0p
    h1v := h0v
    h0p := sp
    h0v := sv
}
vl0 = l0p / l0v
vl1 = l1p / l1v
vl2 = l2p / l2v
vl3 = l3p / l3v
vh0 = h0p / h0v
vh1 = h1p / h1v
vh2 = h2p / h2v
vh3 = h3p / h3v
above = (close > vl0 ? 1 : 0) + (close > vl1 ? 1 : 0) + (close > vl2 ? 1 : 0) + (close > vl3 ? 1 : 0) + (close > vh0 ? 1 : 0) + (close > vh1 ? 1 : 0) + (close > vh2 ? 1 : 0) + (close > vh3 ? 1 : 0)
counted = (math.is_na(vl0) ? 0 : 1) + (math.is_na(vl1) ? 0 : 1) + (math.is_na(vl2) ? 0 : 1) + (math.is_na(vl3) ? 0 : 1) + (math.is_na(vh0) ? 0 : 1) + (math.is_na(vh1) ? 0 : 1) + (math.is_na(vh2) ? 0 : 1) + (math.is_na(vh3) ? 0 : 1)
share = counted > 0 ? above / counted : na
cost_hi = math.max(math.max(vl0, vl1), math.max(vl2, vl3))
cost_lo = math.min(math.min(vl0, vl1), math.min(vl2, vl3))
c_top = render.line(cost_hi, "Cost band top", color = color.transparent, width = 0)
c_bot = render.line(cost_lo, "Cost band bottom", color = color.transparent, width = 0)
render.band(c_top, c_bot, color = color.ramp(math.coalesce(share, 0.5), 0, 1, "pnl", 72), opacity = 1, title = "Buyers' cost band")
render.line(vl3, "Swing low VWAP 4", color = color.translucent(color.emerald.700, 30), width = 1)
render.line(vl2, "Swing low VWAP 3", color = color.translucent(color.emerald.600, 20), width = 1)
render.line(vl1, "Swing low VWAP 2", color = color.translucent(color.emerald.400, 10), width = 1.25)
render.line(vl0, "Swing low VWAP 1", color = color.emerald.300, width = 1.75)
render.line(vh3, "Swing high VWAP 4", color = color.translucent(color.rose.700, 30), width = 1)
render.line(vh2, "Swing high VWAP 3", color = color.translucent(color.rose.600, 20), width = 1)
render.line(vh1, "Swing high VWAP 2", color = color.translucent(color.rose.400, 10), width = 1.25)
render.line(vh0, "Swing high VWAP 1", color = color.rose.300, width = 1.75)
m = margin * ta.atr(14)
up_line = vh0 + m
dn_line = vl0 - m
reclaim_x = close > up_line and close[1] <= up_line[1] and math.is_na(ph)
lost_x = close < dn_line and close[1] >= dn_line[1] and math.is_na(pl)
since_r = ta.bars_since(reclaim_x)
since_l = ta.bars_since(lost_x)
reclaim = reclaim_x and math.coalesce(since_r[1], 1000000) > quiet
lost = lost_x and math.coalesce(since_l[1], 1000000) > quiet
full = counted == 8 and above == 8
since_f = ta.bars_since(full)
full_new = full and math.coalesce(since_f[1], 1000000) > quiet
render.mark(reclaim, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Reclaim", title = "Swing-high VWAP reclaim")
render.mark(lost, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Lost", title = "Swing-low VWAP lost")
render.mark(full_new, shape = shape.diamond, at = anchor.below_bar, color = color.lime.300, size = 0.6, title = "Full support")
render.mark(not math.is_na(pl), shape = shape.circle, at = anchor.below_bar, color = color.translucent(color.emerald.300, 40), size = 0.35, title = "New low anchor")
render.mark(not math.is_na(ph), shape = shape.circle, at = anchor.above_bar, color = color.translucent(color.rose.300, 40), size = 0.35, title = "New high anchor")
alert.rule(reclaim, "Swing-high VWAP reclaim", "Close above the VWAP anchored at the newest swing high")
alert.rule(lost, "Swing-low VWAP lost", "Close below the VWAP anchored at the newest swing low")
alert.rule(full_new, "Full support", "Price is above every anchored VWAP")
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    pr.heading(t, 2, "Anchored VWAP Constellation", "Levels · eight swing-anchored VWAPs")
    pr.row(t, 2, "Lines below price", str.format("{} / {}", above, counted), share > 0.5 ? pr.BULL : pr.BEAR)
    pr.row(t, 3, "Newest low VWAP", str.format("{:.2f} ({:+.2%})", vl0, close / vl0 - 1), close > vl0 ? pr.BULL : pr.BEAR)
    pr.row(t, 4, "Newest high VWAP", str.format("{:.2f} ({:+.2%})", vh0, close / vh0 - 1), close > vh0 ? pr.BULL : pr.BEAR)
    pr.row(t, 5, "Crowd", share > 0.75 ? "In profit, dips defended" : share < 0.25 ? "Trapped, rallies sold" : "Mixed", share > 0.75 ? pr.BULL : share < 0.25 ? pr.BEAR : pr.WARN)
}

Auto Regression Channels

analysis · presets/analysis/levels/auto_regression_channels.qs · uses viz/premium

Regression channels drawn automatically from the market's own turning points. Every confirmed swing high and low is a candidate anchor; from each one a least-squares line is fitted through every close up to the newest bar. Channels whose fit is clean (|Pearson r| above the threshold) are kept, the strongest first, so the chart shows the two or three trends that actually describe price since a structural turn, not an arbitrary number of bars. Each channel has a sigma band (residual standard deviation) or a Raff band (largest residual), a filled body and a projection to the right. The history line is the end point of the best anchored channel, coloured by slope, with its 2-sigma envelope as a GPU gradient band. Reading: price at the far edge of a clean rising channel = stretched, at the near edge = pullback to value; a close outside the band of the strongest channel ends that trend.

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)
auto_regression_channels.qs
analysis "Auto Regression Channels" {
    overlay     = true
    render      = webgpu
    short_title = "ARCH"
    max_objects = 120
}
/// Regression channels drawn automatically from the market's own turning points. Every confirmed
/// swing high and low is a candidate anchor; from each one a least-squares line is fitted through
/// every close up to the newest bar. Channels whose fit is clean (|Pearson r| above the threshold)
/// are kept, the strongest first, so the chart shows the two or three trends that actually
/// describe price since a structural turn, not an arbitrary number of bars.
/// Each channel has a sigma band (residual standard deviation) or a Raff band (largest residual),
/// a filled body and a projection to the right. The history line is the end point of the best
/// anchored channel, coloured by slope, with its 2-sigma envelope as a GPU gradient band.
/// Reading: price at the far edge of a clean rising channel = stretched, at the near edge =
/// pullback to value; a close outside the band of the strongest channel ends that trend.
use "viz/premium" as pr
left    = param.int(10, title = "Swing bars left", min = 2, max = 60, group = "Anchors")
right   = param.int(5, title = "Swing bars right", min = 1, max = 30, group = "Anchors")
min_len = param.int(30, title = "Shortest channel (bars)", min = 10, max = 500, group = "Anchors")
max_len = param.int(600, title = "Longest channel (bars)", min = 50, max = 3000, group = "Anchors")
r_min   = param.float(0.75, title = "Minimum |Pearson r|", min = 0.3, max = 0.99, step = 0.01, group = "Channels")
keep    = param.int(3, title = "Channels drawn", min = 1, max = 6, group = "Channels")
mode    = param.choice("sigma", title = "Band", options = ["sigma", "raff"], group = "Channels")
k_sd    = param.float(2.0, title = "Band width (sigma)", min = 0.5, max = 4, step = 0.1, group = "Channels")
ext     = param.int(30, title = "Projection bars", min = 0, max = 300, group = "Channels")

ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
persist anchors = list.create(int)
if not math.is_na(ph) or not math.is_na(pl) {
    list.push(anchors, bar.index - right)
    if list.size(anchors) > 24 { list.shift(anchors) }
}

// continuous history: the best channel among a geometric ladder of lengths (non-repainting)
ratio = math.pow(max_len / min_len, 1 / 11)
best_r = 0.0
best_v = na
best_s = na
best_sd = na
for k in 0..=11 {
    len = math.round(min_len * math.pow(ratio, k))
    (v, s, _) = stat.linreg(close, len, 0)
    r2 = stat.r_squared(close, len)
    sd = stat.stdev(close - v, len)
    if r2 > best_r {
        best_r := r2
        best_v := v
        best_s := s
        best_sd := sd
    }
}
up = best_s > 0
tone = up ? color.sky.300 : color.amber.300
pc = render.line(best_v, "Best channel end", color = tone, width = 2)
pu = render.line(best_v + k_sd * best_sd, "Upper band", color = color.translucent(color.amber.300, 55), width = 1)
pd = render.line(best_v - k_sd * best_sd, "Lower band", color = color.translucent(color.sky.300, 55), width = 1)
render.band(pu, pc, texture = "gradient", direction = "edges", upper_color = color.amber.500, lower_color = color.indigo.950, opacity = 0.22, title = "Upper half")
render.band(pc, pd, texture = "gradient", direction = "edges", upper_color = color.indigo.950, lower_color = color.sky.500, opacity = 0.22, title = "Lower half")
stretched_up = close > best_v + k_sd * best_sd
stretched_dn = close < best_v - k_sd * best_sd
render.mark(stretched_up and not stretched_up[1], shape = shape.circle, at = anchor.above_bar, color = color.amber.300, size = 0.6, title = "Above the band")
render.mark(stretched_dn and not stretched_dn[1], shape = shape.circle, at = anchor.below_bar, color = color.sky.300, size = 0.6, title = "Below the band")
alert.rule(stretched_up or stretched_dn, "Channel stretch", "Price closed outside the best regression channel")

persist ids = list.create(id)
persist t = draw.table.create(anchor.top_right, 8, 2)
if bar.is_last {
    for i in ids { draw.path.delete(i) }
    list.clear(ids)
    // score every anchor: (|r|, anchor) sorted strongest first
    rs = list.create(float)
    xs0 = list.create(int)
    for a in anchors {
        n = bar.index - a + 1
        if n >= min_len and n <= max_len {
            sx = 0.0
            sy = 0.0
            sxx = 0.0
            sxy = 0.0
            syy = 0.0
            for i in 0..n {
                x = n - 1 - i
                y = close[i]
                sx := sx + x
                sy := sy + y
                sxx := sxx + x * x
                sxy := sxy + x * y
                syy := syy + y * y
            }
            cov = sxy - sx * sy / n
            vx = sxx - sx * sx / n
            vy = syy - sy * sy / n
            r = cov / math.sqrt(math.max(vx * vy, 1e-24))
            if math.abs(r) >= r_min {
                list.push(rs, math.abs(r))
                list.push(xs0, a)
            }
        }
    }
    drawn = 0
    row = 2
    pr.heading(t, 2, "Auto Regression Channels", "Levels · swing-anchored least squares")
    while drawn < keep and list.size(rs) > 0 {
        bi = 0
        for j in 0..list.size(rs) {
            if list.get(rs, j) > list.get(rs, bi) { bi := j }
        }
        a = list.get(xs0, bi)
        rr = list.get(rs, bi)
        list.remove(rs, bi)
        list.remove(xs0, bi)
        n = bar.index - a + 1
        sx = 0.0
        sy = 0.0
        sxx = 0.0
        sxy = 0.0
        for i in 0..n {
            x = n - 1 - i
            sx := sx + x
            sy := sy + close[i]
            sxx := sxx + x * x
            sxy := sxy + x * close[i]
        }
        slope = (sxy - sx * sy / n) / math.max(sxx - sx * sx / n, 1e-12)
        icpt = (sy - slope * sx) / n
        ss = 0.0
        dev = 0.0
        for i in 0..n {
            e = close[i] - (icpt + slope * (n - 1 - i))
            ss := ss + e * e
            dev := math.max(dev, math.abs(e))
        }
        w = mode == "raff" ? dev : k_sd * math.sqrt(ss / n)
        y0 = icpt
        y1 = icpt + slope * (n - 1 + ext)
        x1 = bar.index + ext
        c = slope > 0 ? (drawn == 0 ? color.sky.300 : color.cyan.400) : (drawn == 0 ? color.amber.300 : color.orange.400)
        px = list.create(float)
        py = list.create(float)
        list.push(px, a)
        list.push(py, y0 + w)
        list.push(px, x1)
        list.push(py, y1 + w)
        list.push(px, x1)
        list.push(py, y1 - w)
        list.push(px, a)
        list.push(py, y0 - w)
        list.push(ids, draw.path.create(px, py, color = color.translucent(c, 55), width = 1, closed = true, fill = color.translucent(c, drawn == 0 ? 88 : 93)))
        mx = list.create(float)
        my = list.create(float)
        list.push(mx, a)
        list.push(my, y0)
        list.push(mx, x1)
        list.push(my, y1)
        list.push(ids, draw.path.create(mx, my, color = c, width = drawn == 0 ? 2 : 1))
        pr.row(t, row, str.format("Channel {} ({} bars)", drawn + 1, n), str.format("r {:.2f} · {:+.3%}/bar", rr * math.sign(slope), slope / close), slope > 0 ? pr.BULL : pr.BEAR)
        row += 1
        drawn += 1
    }
    if drawn == 0 { pr.row(t, 2, "No clean channel", str.format("|r| < {:.2f}", r_min), pr.LABEL) }
}

Auto Trendlines

analysis · presets/analysis/levels/auto_trendlines.qs · uses viz/premium

Detects and draws trendlines automatically. Every confirmed swing high is tested against the previous swing highs (and every swing low against the previous lows): a candidate line is kept only if no bar closed through it between its anchors and since, and if enough swings touch it within the tolerance. The line with the most touches wins, so lines are structural, not every pair of pivots. Lines extend to the right until price closes through them by more than the tolerance; a broken line stops at the break, turns dashed and is labelled. Reading: falling resistance (rose) and rising support (emerald) frame the trend; three or more touches make a line significant; a break with a close beyond tolerance is the signal, a retest of the broken line from the other side is the classic entry. Signals: trendline breakouts and breakdowns, with alerts.

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)
auto_trendlines.qs
analysis "Auto Trendlines" {
    overlay     = true
    render      = webgpu
    short_title = "ATL"
    max_objects = 120
}
/// Detects and draws trendlines automatically. Every confirmed swing high is tested against the
/// previous swing highs (and every swing low against the previous lows): a candidate line is kept
/// only if no bar closed through it between its anchors and since, and if enough swings touch it
/// within the tolerance. The line with the most touches wins, so lines are structural, not every
/// pair of pivots. Lines extend to the right until price closes through them by more than the
/// tolerance; a broken line stops at the break, turns dashed and is labelled.
/// Reading: falling resistance (rose) and rising support (emerald) frame the trend; three or more
/// touches make a line significant; a break with a close beyond tolerance is the signal, a
/// retest of the broken line from the other side is the classic entry.
/// Signals: trendline breakouts and breakdowns, with alerts.
use "viz/premium" as pr
left      = param.int(8, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right     = param.int(4, title = "Swing bars right", min = 1, max = 30, group = "Swings")
look      = param.int(6, title = "Swings compared", min = 2, max = 20, group = "Lines")
min_touch = param.int(3, title = "Minimum touches", min = 2, max = 8, group = "Lines")
tol_atr   = param.float(0.3, title = "Tolerance (ATR)", min = 0.05, max = 2, step = 0.05, group = "Lines")
keep      = param.int(3, title = "Lines kept per side", min = 1, max = 10, group = "Lines")
span_max  = param.int(400, title = "Longest line (bars)", min = 50, max = 3000, group = "Lines")

struct TLine {
    x1: int = 0
    y1: float = na
    slope: float = 0
    kind: int = 1
    touches: int = 0
    seg: id = na
    tag: id = na
    broken: bool = false
}

a = ta.atr(50)
tol = tol_atr * a
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
persist hx = list.create(int)
persist hy = list.create(float)
persist lx = list.create(int)
persist ly = list.create(float)
persist lines = list.create(TLine)
now = bar.index

/// Best line through the newest swing (x2, y2) and one of the earlier swings: (found, x1, y1, touches).
/// kind = 1 tests a resistance (no close above), -1 a support (no close below).
fn best_line(kind: int, xs: list<int>, ys: list<float>, x2: int, y2: float, tol: float, look: int, span_max: int, now: int) -> (bool, int, float, int) {
    found = false
    bx = 0
    by = 0.0
    bt = 0
    n = list.size(xs)
    for c in 1..=look {
        j = n - 1 - c
        if j >= 0 {
            x1 = list.get(xs, j)
            y1 = list.get(ys, j)
            if x2 - x1 >= 3 and now - x1 <= span_max {
                slope = (y2 - y1) / (x2 - x1)
                clean = true
                for b in x1..=now {
                    yl = y1 + slope * (b - x1)
                    cl = close[now - b]
                    if kind == 1 and cl > yl + tol { clean := false }
                    if kind == -1 and cl < yl - tol { clean := false }
                    if not clean { break }
                }
                touches = 0
                for m in 0..n {
                    xm = list.get(xs, m)
                    if xm >= x1 {
                        if math.abs(list.get(ys, m) - (y1 + slope * (xm - x1))) <= tol { touches := touches + 1 }
                    }
                }
                if clean and touches > bt {
                    found := true
                    bx := x1
                    by := y1
                    bt := touches
                }
            }
        }
    }
    return (found, bx, by, bt)
}

/// Draws a new line and drops the oldest one of the same side beyond `keep`.
fn add_line(lines: list<TLine>, kind: int, x1: int, y1: float, x2: int, y2: float, touches: int, keep: int) {
    slope = (y2 - y1) / (x2 - x1)
    tone = kind == 1 ? color.rose.400 : color.emerald.400
    seg = draw.segment.create(x1, y1, x2, y2, color = tone, width = touches >= 4 ? 2 : 1.5, ray = ray.right)
    tag = draw.tag.create(x1, y1, str.format("{}x", touches), color = color.translucent(tone, 35), shape = kind == 1 ? shape.tag_down : shape.tag_up, size = 0.7)
    list.push(lines, TLine(x1 = x1, y1 = y1, slope = slope, kind = kind, touches = touches, seg = seg, tag = tag))
    same = 0
    for i in 0..list.size(lines) {
        if list.get(lines, i).kind == kind { same := same + 1 }
    }
    if same > keep {
        for i in 0..list.size(lines) {
            l = list.get(lines, i)
            if l.kind == kind and same > keep {
                draw.segment.delete(l.seg)
                draw.tag.delete(l.tag)
                list.remove(lines, i)
                same := same - 1
                break
            }
        }
    }
}

if not math.is_na(ph) {
    x2 = now - right
    list.push(hx, x2)
    list.push(hy, ph)
    (ok, x1, y1, t) = best_line(1, hx, hy, x2, ph, tol, look, span_max, now)
    if ok and t >= min_touch { add_line(lines, 1, x1, y1, x2, ph, t, keep) }
    if list.size(hx) > 60 {
        list.shift(hx)
        list.shift(hy)
    }
}
if not math.is_na(pl) {
    x2 = now - right
    list.push(lx, x2)
    list.push(ly, pl)
    (ok, x1, y1, t) = best_line(-1, lx, ly, x2, pl, tol, look, span_max, now)
    if ok and t >= min_touch { add_line(lines, -1, x1, y1, x2, pl, t, keep) }
    if list.size(lx) > 60 {
        list.shift(lx)
        list.shift(ly)
    }
}

// breaks: a close beyond the line by more than the tolerance ends it at the break
brk_up = false
brk_dn = false
near_res = na
near_sup = na
for i in 0..list.size(lines) {
    l = list.get(lines, i)
    if not l.broken {
        y = l.y1 + l.slope * (now - l.x1)
        if l.kind == 1 and close > y + tol {
            brk_up := true
            l.broken := true
        }
        if l.kind == -1 and close < y - tol {
            brk_dn := true
            l.broken := true
        }
        if l.broken {
            draw.segment.delete(l.seg)
            l.seg := draw.segment.create(l.x1, l.y1, now, y, color = color.translucent(l.kind == 1 ? color.rose.300 : color.emerald.300, 45), width = 1, stroke = style.dashed)
            draw.tag.update(l.tag, x = now, y = y, text = l.kind == 1 ? "Breakout" : "Breakdown")
            list.set(lines, i, l)
        } else {
            if l.kind == 1 and (math.is_na(near_res) or y < near_res) { near_res := y }
            if l.kind == -1 and (math.is_na(near_sup) or y > near_sup) { near_sup := y }
        }
    }
}
render.line(near_res, "Nearest resistance", color = color.translucent(color.rose.400, 70), width = 1, stroke = style.dotted)
render.line(near_sup, "Nearest support", color = color.translucent(color.emerald.400, 70), width = 1, stroke = style.dotted)
render.mark(brk_up, shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.300, text = "Break", title = "Trendline breakout")
render.mark(brk_dn, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.300, text = "Break", title = "Trendline breakdown")
alert.rule(brk_up, "Trendline breakout", "Price closed above a validated resistance trendline")
alert.rule(brk_dn, "Trendline breakdown", "Price closed below a validated support trendline")
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
    active_r = 0
    active_s = 0
    for l in lines {
        if not l.broken and l.kind == 1 { active_r := active_r + 1 }
        if not l.broken and l.kind == -1 { active_s := active_s + 1 }
    }
    pr.heading(t, 2, "Auto Trendlines", "Levels · swing-validated lines")
    pr.row(t, 2, "Active resistance / support", str.format("{} / {}", active_r, active_s), color.white)
    pr.row(t, 3, "Nearest resistance", math.is_na(near_res) ? "none" : str.format("{:.2f}", near_res), pr.BEAR)
    pr.row(t, 4, "Nearest support", math.is_na(near_sup) ? "none" : str.format("{:.2f}", near_sup), pr.BULL)
}

Fibonacci Confluence Zones

analysis · presets/analysis/levels/fibonacci_confluence_zones.qs · uses viz/premium

Retracement levels from the last five swing legs at once, clustered into zones. A single leg's 38.2 / 50 / 61.8 / 78.6 % levels are weak on their own; where levels from several different legs land within a fraction of an ATR of each other, many traders are watching the same price. Every cluster of two or more levels becomes a zone, its strength is the number of legs that agree (61.8 % counts one and a half), and zones are painted on a plasma ramp from violet (two legs) to gold (five or more). Reading: the brightest zone below price in an uptrend is the most likely pullback target; a close back above it after a probe is a high-quality long. A close through a bright zone on momentum usually runs to the next one. Signals: bounce from a support zone, rejection from a resistance zone, close through a strong zone.

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)
fibonacci_confluence_zones.qs
analysis "Fibonacci Confluence Zones" {
    overlay     = true
    render      = webgpu
    short_title = "FCZ"
    precision   = 2
    max_objects = 80
}
/// Retracement levels from the last five swing legs at once, clustered into zones. A single leg's
/// 38.2 / 50 / 61.8 / 78.6 % levels are weak on their own; where levels from several different
/// legs land within a fraction of an ATR of each other, many traders are watching the same price.
/// Every cluster of two or more levels becomes a zone, its strength is the number of legs that
/// agree (61.8 % counts one and a half), and zones are painted on a plasma ramp from violet (two
/// legs) to gold (five or more).
/// Reading: the brightest zone below price in an uptrend is the most likely pullback target; a
/// close back above it after a probe is a high-quality long. A close through a bright zone on
/// momentum usually runs to the next one. Signals: bounce from a support zone, rejection from a
/// resistance zone, close through a strong zone.
use "viz/premium" as pr
left    = param.int(8, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right   = param.int(4, title = "Swing bars right", min = 1, max = 30, group = "Swings")
tol_atr = param.float(0.35, title = "Cluster tolerance (ATR)", min = 0.05, max = 2, step = 0.05, group = "Zones")
min_s   = param.float(2.0, title = "Minimum strength", min = 1.5, max = 6, step = 0.5, group = "Zones")
show    = param.int(6, title = "Zones drawn", min = 1, max = 16, group = "Zones")
quiet   = param.int(12, title = "Bars between repeats", min = 1, max = 500, group = "Signals")
struct Swing {
    y: float = na
    x: int = 0
}
struct Lvl {
    y: float = na
    w: float = 0
}
struct Zone {
    top: float = na
    bottom: float = na
    mid: float = na
    strength: float = 0
    legs: int = 0
}
a = ta.atr(50)
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
persist swings = list.create(Swing)
persist zones = list.create(Zone)
persist ids = list.create(id)
persist last_hi = true
changed = false
if not math.is_na(ph) {
    // keep swings alternating: a higher high replaces the previous high
    if list.size(swings) > 0 and last_hi {
        prev = list.last(swings)
        if ph > prev.y { list.set(swings, list.size(swings) - 1, Swing(y = ph, x = bar.index - right)) }
    } else {
        list.push(swings, Swing(y = ph, x = bar.index - right))
    }
    last_hi := true
    changed := true
}
if not math.is_na(pl) {
    if list.size(swings) > 0 and not last_hi {
        prev = list.last(swings)
        if pl < prev.y { list.set(swings, list.size(swings) - 1, Swing(y = pl, x = bar.index - right)) }
    } else {
        list.push(swings, Swing(y = pl, x = bar.index - right))
    }
    last_hi := false
    changed := true
}
while list.size(swings) > 6 { list.shift(swings) }
tol = tol_atr * a
if changed and list.size(swings) >= 3 and not math.is_na(tol) {
    lv = list.create(Lvl)
    n = list.size(swings)
    for j in 1..n {
        s0 = list.get(swings, j - 1)
        s1 = list.get(swings, j)
        span = s1.y - s0.y
        for f in [0.382, 0.5, 0.618, 0.786] {
            list.push(lv, Lvl(y = s1.y - span * f, w = f == 0.618 ? 1.5 : 1.0))
        }
    }
    list.sort_by(lv, "y")
    list.clear(zones)
    m = list.size(lv)
    i = 0
    while i < m {
        z0 = list.get(lv, i)
        zt = z0.y
        zb = z0.y
        w = 0.0
        cnt = 0
        j = i
        while j < m {
            q = list.get(lv, j)
            if j > i and q.y - zb > tol { break }
            zt := math.max(zt, q.y)
            w := w + q.w
            cnt := cnt + 1
            j := j + 1
        }
        if w >= min_s and cnt >= 2 {
            half = math.max((zt - zb) / 2, tol * 0.25)
            mid = (zt + zb) / 2
            list.push(zones, Zone(top = mid + half, bottom = mid - half, mid = mid, strength = w, legs = cnt))
        }
        i := j
    }
    list.sort_by(zones, "strength", true)
    while list.size(zones) > show { list.pop(zones) }
    for zid in ids { draw.zone.delete(zid) }
    list.clear(ids)
    x0 = list.get(swings, 0).x
    for z in zones {
        c = color.ramp(math.min(z.strength, 6), 2, 6, "plasma")
        list.push(ids, draw.zone.create(x0, z.top, bar.index + 5, z.bottom, color = color.translucent(c, 72), border_color = color.translucent(c, 25),
                                        text = str.format("Fib x{:.1f}", z.strength), ray = ray.right))
    }
}
// nearest zones around price and the touch signals
sup_top = na
sup_bot = na
sup_s = 0.0
res_top = na
res_bot = na
res_s = 0.0
for z in zones {
    if z.mid < close and (math.is_na(sup_top) or z.top > sup_top) {
        sup_top := z.top
        sup_bot := z.bottom
        sup_s := z.strength
    }
    if z.mid > close and (math.is_na(res_bot) or z.bottom < res_bot) {
        res_top := z.top
        res_bot := z.bottom
        res_s := z.strength
    }
}
render.step(sup_top, "Nearest support zone", color = color.translucent(color.emerald.300, 30), width = 1, stroke = style.dotted)
render.step(res_bot, "Nearest resistance zone", color = color.translucent(color.rose.300, 30), width = 1, stroke = style.dotted)
bounce_x = not math.is_na(sup_top) and low <= sup_top and low >= sup_bot - tol and close > sup_top and close > open
reject_x = not math.is_na(res_bot) and high >= res_bot and high <= res_top + tol and close < res_bot and close < open
thru_up_x = res_s[1] >= 3 and close > res_top[1] + 0.2 * a and close[1] <= res_top[1]
thru_dn_x = sup_s[1] >= 3 and close < sup_bot[1] - 0.2 * a and close[1] >= sup_bot[1]
sb = ta.bars_since(bounce_x)
sr = ta.bars_since(reject_x)
su = ta.bars_since(thru_up_x)
sd = ta.bars_since(thru_dn_x)
bounce = bounce_x and math.coalesce(sb[1], 1000000) > quiet
reject = reject_x and math.coalesce(sr[1], 1000000) > quiet
thru_up = thru_up_x and math.coalesce(su[1], 1000000) > quiet
thru_dn = thru_dn_x and math.coalesce(sd[1], 1000000) > quiet
render.mark(bounce, shape = shape.triangle_up, at = anchor.below_bar, color = color.yellow.300, size = 0.7, title = "Zone bounce")
render.mark(reject, shape = shape.triangle_down, at = anchor.above_bar, color = color.fuchsia.300, size = 0.7, title = "Zone rejection")
render.mark(thru_up, shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.300, title = "Through strong zone up")
render.mark(thru_dn, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.300, title = "Through strong zone down")
alert.rule(bounce, "Zone bounce", "Price probed a Fibonacci confluence zone and closed back above it")
alert.rule(reject, "Zone rejection", "Price probed a Fibonacci confluence zone and closed back below it")
alert.rule(thru_up or thru_dn, "Through strong zone", "Price closed through a confluence zone of strength three or more")
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    pr.heading(t, 2, "Fibonacci Confluence Zones", "Levels · five legs, clustered retracements")
    pr.row(t, 2, "Zones", str.format("{}", list.size(zones)), color.white)
    pr.row(t, 3, "Support zone", math.is_na(sup_top) ? "none" : str.format("{:.2f} .. {:.2f}", sup_bot, sup_top), pr.BULL)
    pr.row(t, 4, "Resistance zone", math.is_na(res_bot) ? "none" : str.format("{:.2f} .. {:.2f}", res_bot, res_top), pr.BEAR)
    pr.row(t, 5, "Strength (support / resistance)", str.format("{:.1f} / {:.1f}", sup_s, res_s), color.amber.300)
}

Log Regression Channel

analysis · presets/analysis/levels/log_regression_channel.qs · uses viz/premium

A regression channel fitted to the logarithm of price, so it describes compounding growth: a trend rising a constant percent per bar is a straight line in log space and a curve on a linear chart. The length is chosen automatically: twelve candidate windows are fitted on every bar and the one with the highest R squared wins. Bands are residual sigmas in log space, so they widen in proportion to price, as volatility does. The history line is the end point of the winning fit; at the newest bar the whole curved channel is drawn from its first bar and projected forward. The dashboard gives the implied growth rate per bar and per hundred bars.

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)
log_regression_channel.qs
analysis "Log Regression Channel" {
    overlay     = true
    render      = webgpu
    short_title = "LRC"
    max_objects = 30
}
/// A regression channel fitted to the logarithm of price, so it describes compounding growth: a
/// trend rising a constant percent per bar is a straight line in log space and a curve on a linear
/// chart. The length is chosen automatically: twelve candidate windows are fitted on every bar and
/// the one with the highest R squared wins. Bands are residual sigmas in log space, so they widen
/// in proportion to price, as volatility does.
/// The history line is the end point of the winning fit; at the newest bar the whole curved
/// channel is drawn from its first bar and projected forward. The dashboard gives the implied
/// growth rate per bar and per hundred bars.
use "viz/premium" as pr
l_min = param.int(40, title = "Shortest window", min = 10, max = 500, group = "Search")
l_max = param.int(800, title = "Longest window", min = 50, max = 4000, group = "Search")
k1    = param.float(1.0, title = "Inner band (sigma)", min = 0.25, max = 4, step = 0.05, group = "Bands")
k2    = param.float(2.0, title = "Outer band (sigma)", min = 0.5, max = 6, step = 0.05, group = "Bands")
ext   = param.int(40, title = "Projection bars", min = 0, max = 400, group = "Bands")
lc = math.log(close)
ratio = math.pow(l_max / l_min, 1 / 11)
best_r = 0.0
best_len = l_min
best_v = na
best_s = na
best_sd = na
for k in 0..=11 {
    len = math.round(l_min * math.pow(ratio, k))
    (v, s, _) = stat.linreg(lc, len, 0)
    r2 = stat.r_squared(lc, len)
    sd = stat.stdev(lc - v, len)
    if r2 > best_r {
        best_r := r2
        best_len := len
        best_v := v
        best_s := s
        best_sd := sd
    }
}
up = best_s > 0
mid = math.exp(best_v)
p_u2 = render.line(math.exp(best_v + k2 * best_sd), "Upper 2", color = color.translucent(color.rose.300, 40), width = 1)
p_u1 = render.line(math.exp(best_v + k1 * best_sd), "Upper 1", color = color.transparent, width = 0)
p_m = render.line(mid, "Growth line", color = up ? color.emerald.300 : color.rose.300, width = 2)
p_l1 = render.line(math.exp(best_v - k1 * best_sd), "Lower 1", color = color.transparent, width = 0)
p_l2 = render.line(math.exp(best_v - k2 * best_sd), "Lower 2", color = color.translucent(color.emerald.300, 40), width = 1)
render.band(p_u2, p_u1, texture = "gradient", direction = "edges", upper_color = color.rose.500, lower_color = color.violet.950, opacity = 0.28, title = "Rich zone")
render.band(p_u1, p_l1, color = up ? color.emerald.500 : color.rose.500, opacity = 0.05, title = "Fair value")
render.band(p_l1, p_l2, texture = "gradient", direction = "edges", upper_color = color.violet.950, lower_color = color.emerald.500, opacity = 0.28, title = "Cheap zone")
z = (lc - best_v) / math.max(best_sd, 1e-12)
render.candle_color(z > k2 ? color.rose.400 : z < -k2 ? color.emerald.400 : na)
alert.rule(ta.cross_up(z, k2) or ta.cross_down(z, -k2), "Log channel extreme", "Price left the 2-sigma log channel")

persist ids = list.create(id)
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last and not math.is_na(best_v) {
    for i in ids { draw.path.delete(i) }
    list.clear(ids)
    stride = math.max(1, math.floor((best_len + ext) / 120))
    for m in [-k2, -k1, 0.0, k1, k2] {
        xs = list.create(float)
        ys = list.create(float)
        j = -ext
        while j <= best_len - 1 {
            list.push(xs, bar.index - j)
            list.push(ys, math.exp(best_v - best_s * j + m * best_sd))
            j += stride
        }
        tone = m == 0 ? (up ? color.emerald.300 : color.rose.300) : m > 0 ? color.translucent(color.rose.300, math.abs(m) == k2 ? 25 : 60) : color.translucent(color.emerald.300, math.abs(m) == k2 ? 25 : 60)
        list.push(ids, draw.path.create(xs, ys, color = tone, width = m == 0 ? 2 : 1))
    }
    pr.heading(t, 2, "Log Regression Channel", "Levels · compounding trend")
    pr.row(t, 2, "Window", str.format("{} bars", best_len), color.white)
    pr.row(t, 3, "Growth per bar", str.format("{:+.3%}", math.exp(best_s) - 1), up ? pr.BULL : pr.BEAR)
    pr.row(t, 4, "Growth per 100 bars", str.format("{:+.1%}", math.exp(100 * best_s) - 1), up ? pr.BULL : pr.BEAR)
    pr.row(t, 5, "Position", str.format("{:+.2f} sigma", z), math.abs(z) > k2 ? pr.WARN : color.white)
}

Polynomial Regression Channel

analysis · presets/analysis/levels/polynomial_regression_channel.qs · uses viz/premium

A quadratic least-squares fit of price over the window, drawn as a curved channel with residual bands and a short projection. A straight regression cannot show a trend that is accelerating or rolling over; the quadratic term can. On history, the end point of the fit is drawn bar by bar, coloured by its curvature: sky when the trend is accelerating up or decelerating down (positive curvature), amber when it is accelerating down or rolling over (negative). Reading: a curvature flip while the slope still points the old way is the earliest sign of a turn; touches of the outer band against the curvature are exhaustion. Signals: curvature flips and outer-band rejections. Method: ordinary least squares on 1, x, x squared.

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)
polynomial_regression_channel.qs
analysis "Polynomial Regression Channel" {
    overlay     = true
    render      = webgpu
    short_title = "PRC"
    max_objects = 20
}
/// A quadratic least-squares fit of price over the window, drawn as a curved channel with residual
/// bands and a short projection. A straight regression cannot show a trend that is accelerating or
/// rolling over; the quadratic term can. On history, the end point of the fit is drawn bar by bar,
/// coloured by its curvature: sky when the trend is accelerating up or decelerating down
/// (positive curvature), amber when it is accelerating down or rolling over (negative).
/// Reading: a curvature flip while the slope still points the old way is the earliest sign of a
/// turn; touches of the outer band against the curvature are exhaustion. Signals: curvature flips
/// and outer-band rejections. Method: ordinary least squares on 1, x, x squared.
use "viz/premium" as pr
len  = param.int(150, title = "Window bars", min = 20, max = 1000, group = "Fit")
k_sd = param.float(2.0, title = "Band (sigma)", min = 0.5, max = 5, step = 0.05, group = "Fit")
ext  = param.int(15, title = "Projection bars", min = 0, max = 100, group = "Fit")
pt_step = param.int(3, title = "Curve point every", min = 1, max = 20, group = "Drawing")
// sums for x = 0 (newest) .. -(len-1)
s0 = 0.0
s1 = 0.0
s2 = 0.0
s3 = 0.0
s4 = 0.0
t0 = 0.0
t1 = 0.0
t2 = 0.0
for i in 0..len {
    y = close[i]
    if not math.is_na(y) {
        x = -i
        s0 := s0 + 1
        s1 := s1 + x
        s2 := s2 + x * x
        s3 := s3 + x * x * x
        s4 := s4 + x * x * x * x
        t0 := t0 + y
        t1 := t1 + x * y
        t2 := t2 + x * x * y
    }
}
// Cramer's rule on the 3x3 normal equations: y = c0 + c1 x + c2 x^2
det = s0 * (s2 * s4 - s3 * s3) - s1 * (s1 * s4 - s3 * s2) + s2 * (s1 * s3 - s2 * s2)
c0 = det != 0 ? (t0 * (s2 * s4 - s3 * s3) - s1 * (t1 * s4 - s3 * t2) + s2 * (t1 * s3 - s2 * t2)) / det : na
c1 = det != 0 ? (s0 * (t1 * s4 - t2 * s3) - t0 * (s1 * s4 - s3 * s2) + s2 * (s1 * t2 - t1 * s2)) / det : na
c2 = det != 0 ? (s0 * (s2 * t2 - s3 * t1) - s1 * (s1 * t2 - s2 * t1) + t0 * (s1 * s3 - s2 * s2)) / det : na
ss = 0.0
for i in 0..len {
    e = close[i] - (c0 - c1 * i + c2 * i * i)
    ss := ss + math.coalesce(e * e, 0)
}
sd = s0 > 3 ? math.sqrt(ss / (s0 - 3)) : na
curv = c2 > 0
p_c = render.line(c0, "Fit end", color = curv ? color.sky.300 : color.amber.300, width = 2)
p_u = render.line(c0 + k_sd * sd, "Upper band", color = color.translucent(color.slate.300, 55), width = 1)
p_l = render.line(c0 - k_sd * sd, "Lower band", color = color.translucent(color.slate.300, 55), width = 1)
render.band(p_u, p_c, texture = "gradient", direction = "edges", upper_color = color.amber.500, lower_color = color.slate.900, opacity = 0.22, title = "Upper half")
render.band(p_c, p_l, texture = "gradient", direction = "edges", upper_color = color.slate.900, lower_color = color.sky.500, opacity = 0.22, title = "Lower half")
flip_up = curv and not curv[1]
flip_dn = not curv and curv[1]
reject_hi = high > c0 + k_sd * sd and close < c0 + k_sd * sd and not curv
reject_lo = low < c0 - k_sd * sd and close > c0 - k_sd * sd and curv
render.mark(flip_up, shape = shape.triangle_up, at = anchor.below_bar, color = color.sky.300, size = 0.7, title = "Curvature turns up")
render.mark(flip_dn, shape = shape.triangle_down, at = anchor.above_bar, color = color.amber.300, size = 0.7, title = "Curvature turns down")
render.mark(reject_hi, shape = shape.circle, at = anchor.above_bar, color = color.rose.300, size = 0.6, title = "Upper band rejection")
render.mark(reject_lo, shape = shape.circle, at = anchor.below_bar, color = color.emerald.300, size = 0.6, title = "Lower band rejection")
alert.rule(flip_up or flip_dn, "Curvature flip", "The quadratic fit changed curvature")
persist ids = list.create(id)
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last and not math.is_na(c0) {
    for i in ids { draw.path.delete(i) }
    list.clear(ids)
    for m in [0.0, k_sd, -k_sd] {
        xs = list.create(float)
        ys = list.create(float)
        for j in 0..=(len + ext) / pt_step {
            i = len - 1 - j * pt_step
            list.push(xs, bar.index - i)
            list.push(ys, c0 - c1 * i + c2 * i * i + m * sd)
        }
        tone = m == 0 ? (curv ? color.sky.300 : color.amber.300) : color.translucent(color.slate.200, 45)
        list.push(ids, draw.path.create(xs, ys, color = tone, width = m == 0 ? 2 : 1))
    }
    pr.heading(t, 2, "Polynomial Regression Channel", "Levels · quadratic least squares")
    pr.row(t, 2, "Slope at newest bar", str.format("{:+.4f}", c1), c1 > 0 ? pr.BULL : pr.BEAR)
    pr.row(t, 3, "Curvature", curv ? "Positive" : "Negative", curv ? color.sky.300 : color.amber.300)
    pr.row(t, 4, "Phase", c1 > 0 ? (curv ? "Accelerating up" : "Rolling over") : (curv ? "Basing" : "Accelerating down"), color.white)
}

Regression Channel Stack

analysis · presets/analysis/levels/regression_channel_stack.qs · uses viz/premium

Three nested regression channels found automatically: a short one (the current swing), a medium one (the trend) and a long one (the primary move). Each searches its own range of windows for the best fit, so each locks onto a real trend at its scale. The three central lines are drawn as GPU series with their 2-sigma envelopes as layered gradient bands; at the newest bar the full channels are drawn as straight segments with their projections. Reading: all three sloping the same way = an aligned trend; a short channel sloping against the medium one, near the medium channel's edge = a pullback ending (or a reversal starting). Marks: short channel turning back in the medium trend's direction at the medium edge.

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_channel_stack.qs
analysis "Regression Channel Stack" {
    overlay     = true
    render      = webgpu
    short_title = "RCS"
    max_objects = 40
}
/// Three nested regression channels found automatically: a short one (the current swing), a
/// medium one (the trend) and a long one (the primary move). Each searches its own range of
/// windows for the best fit, so each locks onto a real trend at its scale. The three central lines
/// are drawn as GPU series with their 2-sigma envelopes as layered gradient bands; at the newest
/// bar the full channels are drawn as straight segments with their projections.
/// Reading: all three sloping the same way = an aligned trend; a short channel sloping against the
/// medium one, near the medium channel's edge = a pullback ending (or a reversal starting).
/// Marks: short channel turning back in the medium trend's direction at the medium edge.
use "viz/premium" as pr
k_sd = param.float(2.0, title = "Band width (sigma)", min = 0.5, max = 4, step = 0.1)
ext  = param.int(25, title = "Projection bars", min = 0, max = 300)

fn best_fit(lo: int, hi: int) -> (series, series, series, series) {
    ratio = math.pow(hi / lo, 1 / 5)
    br = 0.0
    bv = na
    bs = na
    bsd = na
    bl = lo
    for k in 0..=5 {
        len = math.round(lo * math.pow(ratio, k))
        (v, s, _) = stat.linreg(close, len, 0)
        r2 = stat.r_squared(close, len)
        sd = stat.stdev(close - v, len)
        if r2 > br {
            br := r2
            bv := v
            bs := s
            bsd := sd
            bl := len
        }
    }
    return (bv, bs, bsd, bl)
}
(sv, ss, ssd, sl) = best_fit(15, 60)
(mv, ms, msd, ml) = best_fit(60, 240)
(lv, ls, lsd, ll) = best_fit(240, 1000)

fn layer(v: series, s: series, sd: series, k: float, name: text, up: color, dn: color, fade: float) {
    c = s > 0 ? up : dn
    pc = render.line(v, name, color = c, width = 1.5)
    pu = render.line(v + k * sd, name + " upper", color = color.translucent(c, 60), width = 1)
    pd = render.line(v - k * sd, name + " lower", color = color.translucent(c, 60), width = 1)
    render.band(pu, pd, color = c, opacity = fade, title = name + " body")
}
layer(lv, ls, lsd, k_sd, "Long", color.indigo.400, color.fuchsia.400, 0.05)
layer(mv, ms, msd, k_sd, "Medium", color.sky.400, color.amber.400, 0.07)
layer(sv, ss, ssd, k_sd, "Short", color.emerald.300, color.rose.300, 0.09)

aligned_up = ss > 0 and ms > 0 and ls > 0
aligned_dn = ss < 0 and ms < 0 and ls < 0
pull_end_up = ms > 0 and ss > 0 and ss[1] <= 0 and low <= mv - 0.5 * k_sd * msd
pull_end_dn = ms < 0 and ss < 0 and ss[1] >= 0 and high >= mv + 0.5 * k_sd * msd
render.background(aligned_up ? color.translucent(color.emerald.500, 94) : aligned_dn ? color.translucent(color.rose.500, 94) : na)
render.mark(pull_end_up, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, size = 0.8, title = "Pullback over (up)")
render.mark(pull_end_dn, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, size = 0.8, title = "Pullback over (down)")
alert.rule(pull_end_up or pull_end_dn, "Pullback over", "The short channel turned back in the medium trend's direction at its edge")

persist ids = list.create(id)
persist t = draw.table.create(anchor.top_right, 5, 2)
if bar.is_last {
    for i in ids { draw.segment.delete(i) }
    list.clear(ids)
    for k in 0..3 {
        v = k == 0 ? sv : k == 1 ? mv : lv
        s = k == 0 ? ss : k == 1 ? ms : ls
        sd = k == 0 ? ssd : k == 1 ? msd : lsd
        n = k == 0 ? sl : k == 1 ? ml : ll
        if not math.is_na(v) {
            c = k == 0 ? (s > 0 ? color.emerald.300 : color.rose.300) : k == 1 ? (s > 0 ? color.sky.400 : color.amber.400) : (s > 0 ? color.indigo.400 : color.fuchsia.400)
            x0 = bar.index - n + 1
            y0 = v - s * (n - 1)
            for m in [-k_sd, 0.0, k_sd] {
                list.push(ids, draw.segment.create(x0, y0 + m * sd, bar.index + ext, v + s * ext + m * sd, color = m == 0 ? c : color.translucent(c, 45), width = m == 0 ? 2 : 1, stroke = m == 0 ? style.solid : style.dashed))
            }
        }
    }
    pr.heading(t, 2, "Regression Channel Stack", "Levels · three auto-fitted scales")
    pr.row(t, 2, str.format("Short ({} bars)", sl), ss > 0 ? "Rising" : "Falling", ss > 0 ? pr.BULL : pr.BEAR)
    pr.row(t, 3, str.format("Medium ({} bars)", ml), ms > 0 ? "Rising" : "Falling", ms > 0 ? pr.BULL : pr.BEAR)
    pr.row(t, 4, str.format("Long ({} bars)", ll), ls > 0 ? "Rising" : "Falling", ls > 0 ? pr.BULL : pr.BEAR)
}

Standard Error Bands

analysis · presets/analysis/levels/standard_error_bands.qs

Confidence bands for the regression line itself. Unlike sigma bands (where single closes fall), standard error bands show where the trend's end point could be: the residual sigma scaled by the standard error of a prediction at the newest bar, times a t multiple. Narrow bands = a precise trend; a close outside them means the trend estimate itself has moved.

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)
standard_error_bands.qs
analysis "Standard Error Bands" {
    overlay     = true
    render      = webgpu
    short_title = "SEB"
}
/// Confidence bands for the regression line itself. Unlike sigma bands (where single closes
/// fall), standard error bands show where the trend's end point could be: the residual sigma
/// scaled by the standard error of a prediction at the newest bar, times a t multiple. Narrow
/// bands = a precise trend; a close outside them means the trend estimate itself has moved.
len = param.int(50, title = "Regression window", min = 5, max = 2000)
tm  = param.float(2.0, title = "t multiple", min = 0.5, max = 5, step = 0.1)
sm  = param.int(3, title = "Smoothing", min = 1, max = 50)
(v, s, _) = stat.linreg(close, len, 0)
sd = stat.stdev(close - v, len)
// standard error of the fitted value at the newest bar (x = len - 1 in a window of len points)
xbar = (len - 1) / 2
sxx = len * (len * len - 1) / 12
se = sd * math.sqrt(1 / len + (len - 1 - xbar) * (len - 1 - xbar) / sxx)
vs = ta.ema(v, sm)
ses = ta.ema(se, sm)
pu = render.line(vs + tm * ses, "Upper SE", color = color.translucent(color.violet.300, 30), width = 1)
pc = render.line(vs, "Regression", color = s > 0 ? color.sky.300 : color.amber.300, width = 2)
pd = render.line(vs - tm * ses, "Lower SE", color = color.translucent(color.violet.300, 30), width = 1)
render.band(pu, pd, color = color.violet.500, opacity = 0.15, title = "Trend confidence")
render.mark(ta.cross_up(close, vs + tm * ses), shape = shape.arrow_up, at = anchor.above_bar, color = color.sky.300, size = 0.5, title = "Above trend confidence")
render.mark(ta.cross_down(close, vs - tm * ses), shape = shape.arrow_down, at = anchor.below_bar, color = color.amber.300, size = 0.5, title = "Below trend confidence")

Support & Resistance Clusters

analysis · presets/analysis/levels/support_resistance_clusters.qs · uses viz/premium

Support and resistance found by clustering swing points. Every confirmed swing high and low is kept with the volume traded on its bar; nearby swings (within the tolerance, in ATR) are merged into one level whose price is their volume-weighted mean. Each level gets a strength score from its number of touches and its volume, and the strongest levels are drawn as zones from their first touch to the right edge, coloured from cool (weak) to hot (strong) on a perceptual ramp. Reading: a zone with many touches and high volume is where size has defended price before; expect reactions there, and treat a close through it as a structural break. The dotted lines are the nearest levels above and below. Signals: breakouts above the nearest resistance cluster and breakdowns below the nearest support cluster.

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)
support_resistance_clusters.qs
analysis "Support & Resistance Clusters" {
    overlay     = true
    render      = webgpu
    short_title = "SRC"
    max_objects = 60
}
/// Support and resistance found by clustering swing points. Every confirmed swing high and low is
/// kept with the volume traded on its bar; nearby swings (within the tolerance, in ATR) are merged
/// into one level whose price is their volume-weighted mean. Each level gets a strength score from
/// its number of touches and its volume, and the strongest levels are drawn as zones from their
/// first touch to the right edge, coloured from cool (weak) to hot (strong) on a perceptual ramp.
/// Reading: a zone with many touches and high volume is where size has defended price before;
/// expect reactions there, and treat a close through it as a structural break. The dotted lines
/// are the nearest levels above and below. Signals: breakouts above the nearest resistance cluster
/// and breakdowns below the nearest support cluster.
use "viz/premium" as pr
left    = param.int(6, title = "Swing bars left", min = 2, max = 50, group = "Swings")
right   = param.int(3, title = "Swing bars right", min = 1, max = 30, group = "Swings")
memory  = param.int(80, title = "Swings remembered", min = 10, max = 400, group = "Swings")
tol_atr = param.float(0.6, title = "Merge tolerance (ATR)", min = 0.1, max = 3, step = 0.05, group = "Clusters")
show    = param.int(6, title = "Levels drawn", min = 1, max = 20, group = "Clusters")
min_t   = param.int(3, title = "Minimum touches", min = 1, max = 10, group = "Clusters")
margin  = param.float(0.6, title = "Break margin (ATR)", min = 0, max = 3, step = 0.05, group = "Signals")

struct Piv {
    y: float = na
    w: float = 0
    x: int = 0
}
struct Level {
    mid: float = na
    top: float = na
    bottom: float = na
    first: int = 0
    last: int = 0
    touches: int = 0
    volume: float = 0
    strength: float = 0
}

a = ta.atr(50)
tol = tol_atr * a
ph = ta.pivot_high(left, right)
pl = ta.pivot_low(left, right)
persist pivs = list.create(Piv)
persist levels = list.create(Level)
persist near_res = na
persist near_sup = na
vol_at = volume[right]
new_piv = not math.is_na(ph) or not math.is_na(pl)
if not math.is_na(ph) { list.push(pivs, Piv(y = ph, w = vol_at, x = bar.index - right)) }
if not math.is_na(pl) { list.push(pivs, Piv(y = pl, w = vol_at, x = bar.index - right)) }
while list.size(pivs) > memory { list.shift(pivs) }

/// Greedy one-pass clustering of the swings sorted by price.
fn cluster(pivs: list<Piv>, tol: float, min_t: int) -> list<Level> {
    out = list.create(Level)
    s = list.copy(pivs)
    list.sort_by(s, "y")
    n = list.size(s)
    avg_w = 0.0
    for p in s { avg_w := avg_w + p.w }
    avg_w := n > 0 ? avg_w / n : 1
    i = 0
    while i < n {
        p0 = list.get(s, i)
        lv = Level(top = p0.y, bottom = p0.y, first = p0.x, last = p0.x)
        sw = 0.0
        swy = 0.0
        j = i
        // the first swing always joins, so every pass consumes at least one swing
        while j < n {
            p = list.get(s, j)
            if j > i and p.y - p0.y > 2 * tol { break }
            w = math.max(p.w, 1e-9)
            sw := sw + w
            swy := swy + w * p.y
            lv.top := math.max(lv.top, p.y)
            lv.bottom := math.min(lv.bottom, p.y)
            lv.first := math.min(lv.first, p.x)
            lv.last := math.max(lv.last, p.x)
            lv.touches := lv.touches + 1
            j := j + 1
        }
        lv.mid := swy / sw
        lv.volume := sw
        lv.strength := lv.touches * (0.5 + 0.5 * sw / math.max(avg_w * lv.touches, 1e-9))
        if lv.touches >= min_t { list.push(out, lv) }
        i := j
    }
    list.sort_by(out, "strength", true)
    return out
}

if new_piv and not math.is_na(tol) {
    levels := cluster(pivs, tol, min_t)
    near_res := na
    near_sup := na
    // only the strongest levels (the ones drawn) set the break levels
    for k in 0..math.min(list.size(levels), show) {
        lv = list.get(levels, k)
        if lv.mid > close and (math.is_na(near_res) or lv.mid < near_res) { near_res := lv.mid }
        if lv.mid < close and (math.is_na(near_sup) or lv.mid > near_sup) { near_sup := lv.mid }
    }
}
render.line(near_res, "Nearest resistance", color = color.translucent(color.rose.400, 55), width = 1, stroke = style.dotted)
render.line(near_sup, "Nearest support", color = color.translucent(color.emerald.400, 55), width = 1, stroke = style.dotted)
// a true crossing of the level held on the previous bar (rebuilt levels do not re-fire)
res_lv = near_res + margin * a
sup_lv = near_sup - margin * a
brk_up = not math.is_na(res_lv[1]) and close > res_lv[1] and close[1] <= res_lv[1]
brk_dn = not math.is_na(sup_lv[1]) and close < sup_lv[1] and close[1] >= sup_lv[1]
render.mark(brk_up, shape = shape.arrow_up, at = anchor.below_bar, color = color.emerald.300, text = "Break", title = "Cluster breakout")
render.mark(brk_dn, shape = shape.arrow_down, at = anchor.above_bar, color = color.rose.300, text = "Break", title = "Cluster breakdown")
alert.rule(brk_up, "Cluster breakout", "Price closed above the nearest resistance cluster")
alert.rule(brk_dn, "Cluster breakdown", "Price closed below the nearest support cluster")
persist zones = list.create(id)
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    for z in zones { draw.zone.delete(z) }
    list.clear(zones)
    n = math.min(list.size(levels), show)
    top_s = n > 0 ? list.get(levels, 0).strength : 1
    for k in 0..n {
        lv = list.get(levels, k)
        heat = color.ramp(lv.strength / math.max(top_s, 1e-9), 0, 1, "inferno", transparency = 62)
        edge = color.ramp(lv.strength / math.max(top_s, 1e-9), 0, 1, "inferno", transparency = 20)
        pad = 0.15 * a
        role = lv.mid > close ? "R" : "S"
        list.push(zones, draw.zone.create(lv.first, lv.top + pad, bar.index + 8, lv.bottom - pad, color = heat, border_color = edge, text = str.format("{} {:.2f}  {}x", role, lv.mid, lv.touches)))
    }
    pr.heading(t, 2, "Support & Resistance Clusters", "Levels · volume-weighted swing clusters")
    pr.row(t, 2, "Levels found", str.format("{}", list.size(levels)), color.white)
    pr.row(t, 3, "Nearest resistance", math.is_na(near_res) ? "none" : str.format("{:.2f}", near_res), pr.BEAR)
    pr.row(t, 4, "Nearest support", math.is_na(near_sup) ? "none" : str.format("{:.2f}", near_sup), pr.BULL)
    pr.row(t, 5, "Strongest level", n > 0 ? str.format("{:.2f} ({}x)", list.get(levels, 0).mid, list.get(levels, 0).touches) : "none", pr.WARN)
}

Trend Quality Prism

analysis · presets/analysis/levels/trend_quality_prism.qs · uses viz/premium

Eight linear regressions of price, from base bars to eight times longer, drawn as the end points of their fitted lines. The facets between neighbouring regressions are filled and coloured each bar by how well those two windows fit a straight line (mean R squared) on a viridis ramp: dark purple is noise, teal is a usable trend, bright yellow is a clean, linear trend. The order of the facets shows direction: short fits above long fits = rising. Reading: a bright, fanned-out prism with every slope pointing the same way is a trend worth following; the prism collapsing into a dark knot is a range. When the shortest fits cross back through the longer ones while quality is still high, the trend is pausing, not ending. Signals: prism aligned up / down (every slope agrees and quality passes the threshold), and quality lost.

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)
trend_quality_prism.qs
analysis "Trend Quality Prism" {
    overlay     = true
    render      = webgpu
    short_title = "TQP"
    precision   = 2
}
/// Eight linear regressions of price, from `base` bars to eight times longer, drawn as the end
/// points of their fitted lines. The facets between neighbouring regressions are filled and
/// coloured each bar by how well those two windows fit a straight line (mean R squared) on a
/// viridis ramp: dark purple is noise, teal is a usable trend, bright yellow is a clean, linear
/// trend. The order of the facets shows direction: short fits above long fits = rising.
/// Reading: a bright, fanned-out prism with every slope pointing the same way is a trend worth
/// following; the prism collapsing into a dark knot is a range. When the shortest fits cross
/// back through the longer ones while quality is still high, the trend is pausing, not ending.
/// Signals: prism aligned up / down (every slope agrees and quality passes the threshold), and
/// quality lost.
use "viz/premium" as pr
base  = param.int(12, title = "Shortest window", min = 4, max = 200, group = "Prism")
q_min = param.float(0.55, title = "Quality threshold (R squared)", min = 0.1, max = 0.95, step = 0.05, group = "Signals")
pos = 0
neg = 0
q_sum = 0.0
q_prev = na
prev = render.line(close, "Facet edge 0", color = color.transparent, width = 0)
for k in 1..=8 {
    n = base * k
    (v, sl, _) = stat.linreg(close, n, 0)
    q = math.coalesce(stat.r_squared(close, n), 0)
    if sl > 0 { pos := pos + 1 }
    if sl < 0 { neg := neg + 1 }
    q_sum := q_sum + q
    edge = render.line(v, str.format("Regression {}", k), color = color.translucent(color.white, 55 + k * 4), width = 1)
    qm = math.is_na(q_prev) ? q : (q + q_prev) / 2
    render.band(prev, edge, color = color.ramp(qm, 0, 1, "viridis", 35 + k * 4), opacity = 1, title = str.format("Facet {}", k))
    prev := edge
    q_prev := q
}
quality = q_sum / 8
aligned_up = pos == 8 and quality >= q_min
aligned_dn = neg == 8 and quality >= q_min
lost = quality < q_min * 0.6 and quality[1] >= q_min * 0.6
render.mark(aligned_up and not aligned_up[1], shape = shape.triangle_up, at = anchor.below_bar, color = color.yellow.300, text = "Prism", title = "Prism aligned up")
render.mark(aligned_dn and not aligned_dn[1], shape = shape.triangle_down, at = anchor.above_bar, color = color.violet.300, text = "Prism", title = "Prism aligned down")
render.mark(lost, shape = shape.circle, at = anchor.above_bar, color = color.slate.400, size = 0.4, title = "Quality lost")
alert.rule(aligned_up and not aligned_up[1], "Prism aligned up", "Every regression slopes up and the fits are clean")
alert.rule(aligned_dn and not aligned_dn[1], "Prism aligned down", "Every regression slopes down and the fits are clean")
alert.rule(lost, "Quality lost", "Mean R squared fell well below the threshold")
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    pr.heading(t, 2, "Trend Quality Prism", "Levels · eight regressions, R squared facets")
    pr.row(t, 2, "Slopes up / down", str.format("{} / {}", pos, neg), pos > neg ? pr.BULL : pr.BEAR)
    pr.row(t, 3, "Mean R squared", str.format("{:.2f}", quality), quality >= q_min ? color.yellow.300 : pr.LABEL)
    pr.row(t, 4, "State", aligned_up ? "Aligned up" : aligned_dn ? "Aligned down" : quality < q_min * 0.6 ? "Range" : "Mixed", aligned_up ? pr.BULL : aligned_dn ? pr.BEAR : color.white)
    pr.row(t, 5, "Windows", str.format("{} .. {} bars", base, base * 8), pr.LABEL)
}

Volume Density Heatmap

analysis · presets/analysis/levels/volume_density_heatmap.qs · uses viz/premium

Where volume has traded recently, painted behind price. The range of the last len bars is cut into 20 horizontal cells; every bar, each cell gets the share of the window's volume traded at that price (each bar's volume is spread over its high-low range), relative to an even spread. Cells are coloured on an inferno ramp: transparent where little traded, glowing amber where volume concentrated. The line through the brightest cell is the rolling point of control. Reading: bright cells are high-volume nodes where price tends to stall and rotate; dark gaps are low-volume nodes that price crosses quickly. A close through the point of control changes who controls the auction; a break into a dark gap tends to run to the next bright cell. Signals: point-of-control reclaim / loss, entry into a low-volume gap.

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)
volume_density_heatmap.qs
analysis "Volume Density Heatmap" {
    overlay     = true
    render      = webgpu
    short_title = "VDH"
    precision   = 2
}
/// Where volume has traded recently, painted behind price. The range of the last `len` bars is cut
/// into 20 horizontal cells; every bar, each cell gets the share of the window's volume traded at
/// that price (each bar's volume is spread over its high-low range), relative to an even spread.
/// Cells are coloured on an inferno ramp: transparent where little traded, glowing amber where
/// volume concentrated. The line through the brightest cell is the rolling point of control.
/// Reading: bright cells are high-volume nodes where price tends to stall and rotate; dark gaps
/// are low-volume nodes that price crosses quickly. A close through the point of control changes
/// who controls the auction; a break into a dark gap tends to run to the next bright cell.
/// Signals: point-of-control reclaim / loss, entry into a low-volume gap.
use "viz/premium" as pr
len   = param.int(120, title = "Window (bars)", min = 20, max = 600, group = "Profile")
glow  = param.float(2.5, title = "Brightest at (x even)", min = 1.2, max = 6, step = 0.1, group = "Style")
fade  = param.int(55, title = "Cell transparency", min = 0, max = 90, group = "Style")
gap   = param.float(0.45, title = "Low-volume gap (x even)", min = 0.05, max = 0.9, step = 0.05, group = "Signals")
hi = ta.highest(high, len)
lo = ta.lowest(low, len)
span = math.max(hi - lo, 1e-12)
cell = span / 20
total = stat.sum(volume, len)
// one pass over the window: each bar spreads its volume over the cells its range covers
persist cells = array.create(20, 0)
array.fill(cells, 0)
if not math.is_na(total) {
    for i in 0..len {
        h = high[i]
        l = low[i]
        dens = volume[i] / math.max(h - l, cell * 0.05)
        k0 = math.clamp(math.floor((l - lo) / cell), 0, 19)
        k1 = math.clamp(math.floor((h - lo) / cell), 0, 19)
        for k in k0..=k1 {
            ov = math.max(math.min(h, lo + (k + 1) * cell) - math.max(l, lo + k * cell), cell * 0.05)
            array.set(cells, k, array.get(cells, k) + dens * ov)
        }
    }
}
best = 0.0
poc = na
here = 0.0
edge = render.line(lo, "Edge 0", color = color.transparent, width = 0)
for k in 0..=19 {
    c_lo = lo + k * cell
    c_hi = c_lo + cell
    share = total > 0 ? array.get(cells, k) / total * 20 : na
    if share > best {
        best := share
        poc := c_lo + cell / 2
    }
    if close >= c_lo and close < c_hi { here := share }
    top = render.line(c_hi, str.format("Edge {}", k + 1), color = color.transparent, width = 0)
    render.band(top, edge, color = color.ramp(math.clamp(share, 0, glow), 0, glow, "inferno", fade + (share < 0.5 ? 40 : 0)), opacity = 1, title = str.format("Cell {}", k + 1))
    edge := top
}
render.step(poc, "Point of control", color = color.amber.300, width = 1.5)
render.line(hi, "Window high", color = color.translucent(color.white, 70), width = 1, stroke = style.dotted)
render.line(lo, "Window low", color = color.translucent(color.white, 70), width = 1, stroke = style.dotted)
reclaim = close > poc and close[1] <= poc[1]
lose = close < poc and close[1] >= poc[1]
in_gap = here < gap
gap_in = in_gap and not in_gap[1]
render.mark(reclaim, shape = shape.triangle_up, at = anchor.below_bar, color = color.amber.300, size = 0.6, title = "POC reclaim")
render.mark(lose, shape = shape.triangle_down, at = anchor.above_bar, color = color.orange.500, size = 0.6, title = "POC loss")
render.mark(gap_in, shape = shape.circle, at = anchor.above_bar, color = color.sky.300, size = 0.45, title = "Low-volume gap")
alert.rule(reclaim, "POC reclaim", "Close back above the rolling point of control")
alert.rule(lose, "POC loss", "Close below the rolling point of control")
alert.rule(gap_in, "Low-volume gap", "Price entered a low-volume cell")
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    pr.heading(t, 2, "Volume Density Heatmap", "Levels · rolling volume at price")
    pr.row(t, 2, "Point of control", str.format("{:.2f}", poc), color.amber.300)
    pr.row(t, 3, "Price vs POC", str.format("{:+.2%}", close / poc - 1), close > poc ? pr.BULL : pr.BEAR)
    pr.row(t, 4, "Density here", str.format("{:.2f} x even", here), here < gap ? color.sky.300 : here > glow * 0.6 ? color.amber.300 : color.white)
    pr.row(t, 5, "Window range", str.format("{:.2f} .. {:.2f}", lo, hi), pr.LABEL)
}

Volume Profile Pro

analysis · presets/analysis/levels/volume_profile_pro.qs · uses viz/premium

A complete auction view on the price chart. The visible-range volume profile is drawn by the chart beside the price axis; the developing session point of control (POC), value area high and low are drawn bar by bar with an edge-anchored value-area band; and every finished session leaves its final POC behind as a "naked POC" line that extends right until price trades back through it. Unvisited POCs are strong magnets and reaction levels. Reading: price inside value = balance (fade the edges); acceptance outside value (two closes beyond VAH or VAL) = imbalance and the start of a move towards the next naked POC; a POC that migrates with price confirms the trend. Signals: value-area acceptance, POC migration and naked POC tests. Method: Steidlmayer market profile, volume at price.

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)
volume_profile_pro.qs
analysis "Volume Profile Pro" {
    overlay     = true
    render      = webgpu
    short_title = "VPP"
    max_objects = 80
}
/// A complete auction view on the price chart. The visible-range volume profile is drawn by the
/// chart beside the price axis; the developing session point of control (POC), value area high
/// and low are drawn bar by bar with an edge-anchored value-area band; and every finished session
/// leaves its final POC behind as a "naked POC" line that extends right until price trades back
/// through it. Unvisited POCs are strong magnets and reaction levels.
/// Reading: price inside value = balance (fade the edges); acceptance outside value (two closes
/// beyond VAH or VAL) = imbalance and the start of a move towards the next naked POC; a POC that
/// migrates with price confirms the trend. Signals: value-area acceptance, POC migration and naked
/// POC tests. Method: Steidlmayer market profile, volume at price.
use "viz/premium" as pr
rows    = param.int(48, title = "Profile rows", min = 10, max = 200, group = "Profile")
va      = param.float(0.7, title = "Value area", min = 0.5, max = 0.95, step = 0.01, group = "Profile")
width   = param.int(140, title = "Profile width (px)", min = 40, max = 400, group = "Profile")
naked_n = param.int(6, title = "Naked POCs kept", min = 0, max = 30, group = "Levels")
render.profile.volume(rows = rows, value_area = va, width = width, position = "right", period = "visible", title = "Visible-range profile")
(poc, vah, val) = ta.volume_profile(rows, va, "session")
p_vah = render.step(vah, "Value area high", color = color.translucent(color.violet.300, 30), width = 1)
p_poc = render.step(poc, "Developing POC", color = color.amber.300, width = 2)
p_val = render.step(val, "Value area low", color = color.translucent(color.violet.300, 30), width = 1)
render.band(p_vah, p_poc, texture = "gradient", direction = "edges", upper_color = color.violet.500, lower_color = color.amber.500, opacity = 0.12, title = "Upper value")
render.band(p_poc, p_val, texture = "gradient", direction = "edges", upper_color = color.amber.500, lower_color = color.violet.500, opacity = 0.12, title = "Lower value")
// acceptance outside value: two closes beyond the edge
acc_up = close > vah and close[1] > vah[1] and not (close[2] > vah[2])
acc_dn = close < val and close[1] < val[1] and not (close[2] < val[2])
mig_up = poc > poc[1] and close > poc
mig_dn = poc < poc[1] and close < poc
render.mark(acc_up, shape = shape.triangle_up, at = anchor.below_bar, color = color.emerald.300, text = "Accept", title = "Acceptance above value")
render.mark(acc_dn, shape = shape.triangle_down, at = anchor.above_bar, color = color.rose.300, text = "Accept", title = "Acceptance below value")
render.mark(mig_up, shape = shape.circle, at = anchor.below_bar, color = color.amber.200, size = 0.4, title = "POC migrates up")
render.mark(mig_dn, shape = shape.circle, at = anchor.above_bar, color = color.amber.200, size = 0.4, title = "POC migrates down")
alert.rule(acc_up or acc_dn, "Value acceptance", "Two closes outside the session value area")
// naked POCs: the final POC of each finished session until price trades through it
struct Naked {
    price: float = na
    x: int = 0
    seg: id = na
    tag: id = na
}
dom = time.day(time)
new_session = dom != dom[1] and bar.index > 0
persist naked = list.create(Naked)
tested = false
if new_session and not math.is_na(poc[1]) and naked_n > 0 {
    p = poc[1]
    seg = draw.segment.create(bar.index - 1, p, bar.index, p, color = color.translucent(color.amber.300, 25), width = 1, stroke = style.dashed, ray = ray.right)
    tag = draw.tag.create(bar.index - 1, p, str.format("nPOC {:.2f}", p), color = color.translucent(color.amber.400, 30), shape = shape.tag, size = 0.7)
    list.push(naked, Naked(price = p, x = bar.index - 1, seg = seg, tag = tag))
    if list.size(naked) > naked_n {
        old = list.shift(naked)
        draw.segment.delete(old.seg)
        draw.tag.delete(old.tag)
    }
}
i = 0
while i < list.size(naked) {
    nk = list.get(naked, i)
    if bar.index > nk.x + 1 and low <= nk.price and high >= nk.price {
        draw.segment.delete(nk.seg)
        draw.tag.delete(nk.tag)
        list.remove(naked, i)
        tested := true
    } else {
        i := i + 1
    }
}
render.mark(tested, shape = shape.diamond, at = anchor.above_bar, color = color.amber.300, size = 0.6, title = "Naked POC tested")
alert.rule(tested, "Naked POC tested", "Price traded back to an unvisited session POC")
persist t = draw.table.create(anchor.top_right, 6, 2)
if bar.is_last {
    pos = close > vah ? "Above value" : close < val ? "Below value" : "Inside value"
    pr.heading(t, 2, "Volume Profile Pro", "Levels · session auction")
    pr.row(t, 2, "Developing POC", str.format("{:.2f}", poc), color.amber.300)
    pr.row(t, 3, "Value area", str.format("{:.2f} - {:.2f}", val, vah), color.violet.300)
    pr.row(t, 4, "Position", pos, close > vah ? pr.BULL : close < val ? pr.BEAR : color.white)
    pr.row(t, 5, "Naked POCs", str.format("{}", list.size(naked)), color.white)
}

© 2026 Quatick Technologies. All rights reserved.