Indicator · Pine Script v6 · On the chart

IQ Trend Beams

by ankurksoni· code credited to Trading-IQ

A price-chart overlay that fits trend support and resistance channel lines using a Kalman-filter based engine fed by intrabar volume and price data.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code1,521
Settings12inputs
LicenceNone statedfile: MPL-2.0
Last update2 days ago27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,521 lines of Pine Script v6 from trading view/iqTrendBeams.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

iqTrendBeams.pine
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Trading-IQ

//@version=6
indicator("IQ Trend Beams [TradingIQ]", overlay = true, max_lines_count = 500, max_labels_count = 100, max_polylines_count = 100, calc_bars_count = 5000)


// HEAD {

// enums {

//@enum the tempo: how quickly lines lock, break, and re-form
enum Mode
    fast = "Fast"
    medium = "Medium"
    slow = "Slow"

//@enum how much data the engine reads: the fit window and the intrabar sample rate
enum Preset
    fast = "Fast"
    balanced = "Balanced"
    precision = "Precision"
    extra = "Extra"
    max = "Max"

// enums }

// types {

//@type user configuration: tempo behavior, data depth, layers, palette
type Config
    int fit_cap
    int lock_k
    float lock_jit
    int lock_min_span
    int break_bars
    float pool_h
    float surv_h
    float p_hyst
    float disp_a

    bool show_sketch
    bool show_archive
    bool show_badges
    bool show_pred
    bool show_sup
    bool show_res

    color bull_color
    color bear_color
    color chrome_color
    float contrast

//@type one run-length hypothesis: a local-linear-trend Kalman state over its flavor's
// hug-side quantile stream (Sarkka ch. 4); the emission is symmetric - asymmetry lives
// in the audit, never in the filter
type Particle
    int id
    int side
    int age = 0
    float logw = 0.0
    float m0
    float m1
    float p00
    float p01
    float p11

//@type the fixed-lag RTS smoother (Sarkka ch. 8): the linear-phase slope reference
type Smoother
    float[] mf0
    float[] mf1
    float[] pf00
    float[] pf01
    float[] pf11
    float[] mp0
    float[] mp1
    float[] pp00
    float[] pp01
    float[] pp11

//@type one assistant line
//@field state 0 = forming (free to move), 1 = locked (frozen forever), 2 = broken (restyled, never moved)
//@field a_disp the displayed geometry, a one-pole behind the fitted a/b while forming
type AsLine
    int side
    int state = 0
    int anchor_t
    float anchor_y
    float a
    float b
    float a_disp
    float b_disp
    float prior_b
    int born
    int lock_t = -1
    int break_t = -1
    int end_bar
    float p_alive = 0.95
    float pen_avg = 0.1
    int touches = 0
    int still = 0
    line[] strokes
    label[] tags

//@type one side of the duo: its locked ink, its forming sketch, its retired history
type Channel
    int side
    AsLine ink = na
    AsLine sketch = na
    AsLine[] archive
    int break_run = 0

//@type engine telemetry: volatility, volume, and the learned slope population
type Stats
    float atr = na
    float atr_fast = na
    float vbar = na
    float r_mult = 1.0
    int ang_n = 0
    float ang_mean = 0.0
    float ang_m2 = 0.0
    float prev_lo = na
    float prev_hi = na
    int next_id = 0
    float vr = 1.0

//@type the volume tape: intrabar (price, volume) prints and the per-bar rings cut from them
type Tape
    int slots
    float[] prints_p
    float[] prints_v
    float[] hq_lo
    float[] hq_hi
    float[] atr
    float[] unit
    float y_lo = na
    float y_hi = na
    float y_mid = na

//@type the live-edge forecast drawings
type Forecast
    polyline[] polys
    line[] levels

// Oklab: a perceptually-uniform colour space - smooth blends, vibrancy independent of lightness
type Ok
    float L
    float a
    float b

//@type the assistant: shared swarm, both channels, tape, telemetry, forecast
type Engine
    Config config
    Stats stats
    Smoother smoother
    Particle[] pool
    Channel sup
    Channel res
    Tape tape
    Forecast cast

// types }

// constants {

// engine constants - calibrated offline in research/; deliberately not inputs
var const int d_lag = 3
var const float nu_beta = 11.675088162973507
var const int k_particles = 16
var const float kv_kick = 2.4138626045615337
var const float tau = 0.10           // wick-through design target: volume allowed beyond the line
var const float lam = 4.0
var const float gamma_lev = 1.0
var const float tau_anchor = 0.25
var const float atr_alpha = 0.05
var const float rho_q = 0.1
var const float q_obs = 0.25
var const float ang_max_z = 2.798201551028527
var const float eps_touch = 0.21268271202843048 // tangency: hug quantile within this many ATR
var const int touch_sep = 4
var const int lock_min_touch = 2
var const float lock_swarm = 0.3530581985159258
var const float break_close = 0.16     // body-through depth (ATR) counting toward a break -
                                     // measured against the bar's volume-weighted center
// lock stillness, break persistence, hazards, hysteresis, and display smoothing are
// tempo values - see the Mode table in inputs
var const float w_touch = 2.723791354844021 // perceptual weights (obj-1.714 search config)
var const float w_span = 0.8287819823204836
var const float w_cut = 5.75311478513242
var const float w_gap = 0.11560527439640671
var const float w_ang = 0.31156056655775244
var const int warmup = 60
var const int archive_keep = 6
var const int pred_bins = 41         // prediction: cross-trend residual bins (KDE-smoothed)
var const float pred_span = 2.5      // ...over +/- this many ATR of the line
var const int kde_radius = 4         // gaussian smoothing half-width (bins) for the density
var const float kde_sigma = 1.2      // ...its std (bins): binned-KDE, light - keep the peaks
var const int surv_kmax = 120        // survival-median search horizon, bars
var const int pred_kmax = 26         // drawn forecast horizon cap, bars
var const float atr_fast_a = 0.30    // fast-ATR EMA rate for the dual-timescale unit
var const float swarm_fast = 0.70    // swarm agreement that fast-tracks stillness
var const int retro_max = 60         // look-back-at-lock cap, bars
var const float retro_pen = 0.22119743146422685 // a look-back bar violates if this volume breaks the line
var const int h_cross = 12           // a lock is refused if it inverts the channel within this

// constants }

// survival head {

// Pretrained discrete-time break hazard
// holdout C-index 0.87). Per locked bar it returns P(break this bar) from the line's age,
// recent wick-through, out-of-channel gap, hug distance, and volume ratio - the learned
// replacement for the old hand-tuned evidence decay. The geomspaced age one-hots and the
// standardization mirror the Python inference (surv_hazard) exactly; clamp inlined because
// the helper clamp() is defined below this block.
var float[] surv_edges = array.from(math.pow(40.0, 0.0 / 6.0), math.pow(40.0, 1.0 / 6.0), math.pow(40.0, 2.0 / 6.0), math.pow(40.0, 3.0 / 6.0), math.pow(40.0, 4.0 / 6.0), math.pow(40.0, 5.0 / 6.0), math.pow(40.0, 6.0 / 6.0))
var float[] surv_mu = array.from(0.0580259222333001, 0.11585244267198405, 0.1373878364905284, 0.19222333000997008, 0.25044865403788635, 0.246061814556331, 0.13321633698903285, 0.9346353818544368, 2.9445011924227282, 0.9856438285144645)
var float[] surv_sd = array.from(0.23379246049066466, 0.3200478936017564, 0.3442563272838727, 0.39404761312541825, 0.4332714227000338, 0.4307149846169415, 0.3190776763414699, 1.4453618948065203, 2.241380002659612, 0.6528508878633361)
var float[] surv_w = array.from(-5.800688062439102, -0.9242006402506199, -0.08099528548253737, -0.15298007520035634, -0.1323053656138756, 0.15848003788540396, 0.6457347039010187, 2.4916877986226345, 0.7366477413898836, 1.4319242563204195, -0.5857458024380247)

//@function per-bar break hazard from the pretrained survival head
//@param age bars since the line locked
//@param pen bar volume fraction beyond the line
//@param out_gap hug distance beyond the channel, dual-ATR units, minus 2.5, floored at 0
//@param gap_hug absolute hug distance from the line, raw-ATR units
//@param vol_ratio bar volume against its own running average
//@returns P(break this bar) in (0, 1)
surv_hazard(int age, float pen, float out_gap, float gap_hug, float vol_ratio)=>
    int nb = 6
    float a = math.max(age, 1)
    float z = surv_w.get(0)
    for int b = 0 to nb - 1
        bool active = a >= surv_edges.get(b) and a < surv_edges.get(b + 1)
        if b == nb - 1 and a >= surv_edges.get(nb)
            active := true
        float oh = active ? 1.0 : 0.0
        z += surv_w.get(b + 1) * ((oh - surv_mu.get(b)) / surv_sd.get(b))
    z += surv_w.get(7) * ((pen - surv_mu.get(6)) / surv_sd.get(6))
    z += surv_w.get(8) * ((out_gap - surv_mu.get(7)) / surv_sd.get(7))
    z += surv_w.get(9) * ((gap_hug - surv_mu.get(8)) / surv_sd.get(8))
    z += surv_w.get(10) * ((vol_ratio - surv_mu.get(9)) / surv_sd.get(9))
    1.0 / (1.0 + math.exp(-math.max(math.min(z, 30.0), -30.0)))

// survival head }

// helpers {

//@function clamps source into [min, max]
clamp(float source, float min, float max)=> math.max(math.min(source, max), min)

//@function hyperbolic tangent
tanh(float x)=>
    float e2 = math.exp(2.0 * clamp(x, -20.0, 20.0))
    (e2 - 1.0) / (e2 + 1.0)

//@function rescales a transparency by its remaining headroom
method scale_alpha(float self, float alpha = 80)=>
    float invert = 100 - alpha
    self * invert / 100

//@function a copy of the color at a new composed transparency
method new_alpha(color self, float alpha = 80)=>
    color.new(self, alpha + color.t(self).scale_alpha(alpha))

//@function sRGB companding + a clamp, for the Oklab transform
clamp01(float v)=> math.max(0.0, math.min(1.0, v))
srgb_to_linear(float c)=> c <= 0.04045 ? c / 12.92 : math.pow((c + 0.055) / 1.055, 2.4)
linear_to_srgb(float c)=> c <= 0.0031308 ? c * 12.92 : 1.055 * math.pow(c, 1.0 / 2.4) - 0.055

//@function two-argument arctangent - for Oklab's polar hue
atan2(float y, float x)=> x > 0 ? math.atan(y / x) : x < 0 ? (y >= 0 ? math.atan(y / x) + math.pi : math.atan(y / x) - math.pi) : (y > 0 ? math.pi / 2.0 : y < 0 ? -math.pi / 2.0 : 0.0)

//@function sRGB colour → Oklab (perceptually-uniform: blends stay smooth, chroma decouples from lightness)
to_ok(color c)=>
    float r = srgb_to_linear(color.r(c) / 255.0)
    float g = srgb_to_linear(color.g(c) / 255.0)
    float bl = srgb_to_linear(color.b(c) / 255.0)
    float l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * bl
    float m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * bl
    float s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * bl
    float l_ = math.sign(l) * math.pow(math.abs(l), 1.0 / 3.0)
    float m_ = math.sign(m) * math.pow(math.abs(m), 1.0 / 3.0)
    float s_ = math.sign(s) * math.pow(math.abs(s), 1.0 / 3.0)
    Ok.new(0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_, 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_, 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_)

//@function Oklab → sRGB colour (opaque)
method col(Ok self)=>
    float l_ = self.L + 0.3963377774 * self.a + 0.2158037573 * self.b
    float m_ = self.L - 0.1055613458 * self.a - 0.0638541728 * self.b
    float s_ = self.L - 0.0894841775 * self.a - 1.2914855480 * self.b
    float l = l_ * l_ * l_
    float m = m_ * m_ * m_
    float s = s_ * s_ * s_
    float r  =  4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
    float g  = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
    float bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
    color.rgb(linear_to_srgb(clamp01(r)) * 255, linear_to_srgb(clamp01(g)) * 255, linear_to_srgb(clamp01(bl)) * 255, 0)

//@function rotate hue (deg), shift lightness, scale chroma in Oklab's polar form - the vibrancy knob
method roll(Ok self, float dhue, float dL, float cmul)=>
    float c  = math.sqrt(self.a * self.a + self.b * self.b)
    float h  = atan2(self.b, self.a) + dhue * math.pi / 180.0
    float c2 = c * cmul
    Ok.new(clamp01(self.L + dL), c2 * math.cos(h), c2 * math.sin(h))

//@function weighted quantile: linear interpolation along the cumulative-weight curve
wq(float[] vals, float[] wts, float q)=>
    int[] order = vals.sort_indices()
    float total = 0.0
    for w in wts
        total += w
    float target = q * total
    float cum_prev = 0.0
    float v_prev = na
    float out = vals.get(order.get(order.size() - 1))
    for idx in order
        float cum = cum_prev + wts.get(idx)
        float v = vals.get(idx)
        if cum >= target
            out := na(v_prev) or cum <= cum_prev ? v : v_prev + (v - v_prev) * (target - cum_prev) / (cum - cum_prev)
            break
        cum_prev := cum
        v_prev := v
    out

//@function reads a one-slot-per-bar ring at chart bar t; na outside the ring
method ring_at(float[] self, int t)=>
    int idx = self.size() - 1 - (bar_index - t)
    idx >= 0 and idx < self.size() ? self.get(idx) : na

//@function pushes one value, evicting the oldest beyond cap
method push_cap(float[] self, float v, int cap)=>
    self.push(v)
    if self.size() > cap
        self.shift()
    self

//@function appends a bar of prints, evicting the oldest beyond cap
method push_bar(float[] self, float[] fresh, int cap)=>
    for f in fresh
        self.push(f)
    while self.size() > cap
        self.shift()
    self

//@function predicted remaining length in bars: where the survival curve, started at the
// element's confidence, crosses one half. The search horizon is wider than the drawn
// forecast - callers cap for geometry, so patient tempos are not censored into the
// chart cap and maturity stays honest. The 3-bar minimum is a display floor, not part
// of the estimator - low-confidence elements would otherwise forecast one bar.
surv_len(float p0, float h)=>
    int k_len = surv_kmax
    float s_run = p0
    for int k = 1 to surv_kmax
        s_run := s_run * (1.0 - h)
        if s_run < 0.5 and k_len == surv_kmax
            k_len := math.max(k, 3)
    k_len

//@function scales designed opacity (100 - alpha) by the contrast dial, so faint stays
// faint relative to bold
contrast_alpha(float alpha, float c)=> clamp(100.0 - (100.0 - alpha) * c, 0.0, 100.0)

//@function price offset of residual bin i from the line, in chart units
bin_off(int i, float atr_unit)=> ((2.0 * i / (pred_bins - 1)) - 1.0) * pred_span * atr_unit

// helpers }

// tape {

//@function resamples one chart bar's lower-timeframe prints into a fixed number of
// (price, volume) slots; without intrabar data the bar's volume spreads over its range
resample(float[] ltf_p, float[] ltf_v, int slots)=>
    float[] price = array.new<float>(slots, close)
    float[] vol = array.new<float>(slots, 0.0)
    int n = na(ltf_p) ? 0 : ltf_p.size()
    if n == 0
        float share = nz(volume) / slots
        for int k = 0 to slots - 1
            price.set(k, low + (high - low) * (k + 0.5) / slots)
            vol.set(k, share)
    else
        float[] moment = array.new<float>(slots, 0.0)
        for int i = 0 to n - 1
            int slot = math.min(slots - 1, i * slots / n)
            float v = math.max(nz(ltf_v.get(i)), 1e-9)
            vol.set(slot, vol.get(slot) + v)
            moment.set(slot, moment.get(slot) + v * nz(ltf_p.get(i), close))
        for int k = 0 to slots - 1
            if vol.get(k) > 0
                price.set(k, moment.get(k) / vol.get(k))
    [price, vol]

//@function ingests one bar of prints into the rings and refreshes the bar's quantile facts
method ingest(Tape self, float[] bar_p, float[] bar_v, float atr_unit, float unit_now, int ring_cap)=>
    self.prints_p.push_bar(bar_p, ring_cap * self.slots)
    self.prints_v.push_bar(bar_v, ring_cap * self.slots)
    self.y_lo := wq(bar_p, bar_v, q_obs)
    self.y_hi := wq(bar_p, bar_v, 1.0 - q_obs)
    self.hq_lo.push_cap(self.y_lo, ring_cap)
    self.hq_hi.push_cap(self.y_hi, ring_cap)
    self.atr.push_cap(atr_unit, ring_cap)
    self.unit.push_cap(unit_now, ring_cap)
    float w_tot = 0.0
    float wp = 0.0
    for int k = 0 to bar_p.size() - 1
        w_tot += bar_v.get(k)
        wp += bar_v.get(k) * bar_p.get(k)
    self.y_mid := w_tot > 0 ? wp / w_tot : close
    self

//@function how many whole bars of prints the tape currently holds
method bars_held(Tape self)=> self.prints_p.size() / self.slots

//@function the hug-side quantile ring for one side
method hug(Tape self, int side)=> side > 0 ? self.hq_lo : self.hq_hi

//@function the current bar's hug-side quantile for one side
method hug_now(Tape self, int side)=> side > 0 ? self.y_lo : self.y_hi

//@function volume fraction of bar t's prints beyond a line value, on the broken side
method pen(Tape self, int t, float line_y, int side)=>
    int size = self.prints_p.size()
    int base = size - (bar_index - t + 1) * self.slots
    float beyond = 0.0
    float tot = 0.0
    if base >= 0
        for int k = 0 to self.slots - 1
            float p = self.prints_p.get(base + k)
            float w = self.prints_v.get(base + k)
            tot += w
            beyond += (side > 0 ? p < line_y : p > line_y) ? w : 0.0
    tot > 0 ? beyond / tot : 0.0

// tape }

// smoother {

//@function one Kalman filter step over the volume-weighted bar center; keeps a fixed lag
// of filtered and predicted moments for the backward pass
//@returns [innovation, innovation variance] for noise adaptation
method step(Smoother self, float y, float r_scale, int lag)=>
    float innov = 0.0
    float s_out = r_scale
    if self.mf0.size() == 0
        self.mf0.push(y),      self.mf1.push(0.0)
        self.pf00.push(r_scale * 10.0), self.pf01.push(0.0), self.pf11.push(r_scale * 0.1)
        self.mp0.push(y),      self.mp1.push(0.0)
        self.pp00.push(r_scale * 10.0), self.pp01.push(0.0), self.pp11.push(r_scale * 0.1)
    else
        int last = self.mf0.size() - 1
        float m0 = self.mf0.get(last)
        float m1 = self.mf1.get(last)
        float p00 = self.pf00.get(last)
        float p01 = self.pf01.get(last)
        float p11 = self.pf11.get(last)
        float q = rho_q * r_scale
        float mp0 = m0 + m1
        float mp1 = m1
        float pp00 = p00 + 2 * p01 + p11 + q / 3.0
        float pp01 = p01 + p11 + q * 0.5
        float pp11 = p11 + q
        float s = pp00 + r_scale
        innov := y - mp0
        s_out := s
        float k0 = pp00 / s
        float k1 = pp01 / s
        self.mf0.push(mp0 + k0 * innov),  self.mf1.push(mp1 + k1 * innov)
        self.pf00.push(pp00 - k0 * pp00), self.pf01.push(pp01 - k0 * pp01), self.pf11.push(pp11 - k1 * pp01)
        self.mp0.push(mp0),  self.mp1.push(mp1)
        self.pp00.push(pp00), self.pp01.push(pp01), self.pp11.push(pp11)
        if self.mf0.size() > lag + 1
            self.mf0.shift(),  self.mf1.shift()
            self.pf00.shift(), self.pf01.shift(), self.pf11.shift()
            self.mp0.shift(),  self.mp1.shift()
            self.pp00.shift(), self.pp01.shift(), self.pp11.shift()
    [innov, s_out]

//@function the RTS backward pass over the stored lag
//@returns [smoothed level, smoothed slope]
method smoothed(Smoother self)=>
    int n = self.mf0.size()
    float s0 = n > 0 ? self.mf0.get(n - 1) : 0.0
    float s1 = n > 0 ? self.mf1.get(n - 1) : 0.0
    if n >= 2
        for int k = n - 2 to 0
            float a11 = self.pf00.get(k) + self.pf01.get(k)
            float a12 = self.pf01.get(k)
            float a21 = self.pf01.get(k) + self.pf11.get(k)
            float a22 = self.pf11.get(k)
            float b11 = self.pp00.get(k + 1)
            float b12 = self.pp01.get(k + 1)
            float b22 = self.pp11.get(k + 1)
            float det = b11 * b22 - b12 * b12
            if math.abs(det) > 1e-18
                float i11 = b22 / det
                float i12 = -b12 / det
                float i22 = b11 / det
                float g11 = a11 * i11 + a12 * i12
                float g12 = a11 * i12 + a12 * i22
                float g21 = a21 * i11 + a22 * i12
                float g22 = a21 * i12 + a22 * i22
                float d0 = s0 - self.mp0.get(k + 1)
                float d1 = s1 - self.mp1.get(k + 1)
                s0 := self.mf0.get(k) + g11 * d0 + g12 * d1
                s1 := self.mf1.get(k) + g21 * d0 + g22 * d1
    [s0, s1]

// smoother }

// swarm {

//@function one Kalman step of a run-length hypothesis over its flavor's hug quantile.
// Volume-scaled level diffusion (Clark-inspired, not a full operational-time
// substitution): a high-volume bar lets the level move more; the slope stays stiff
// in wall time.
//@returns the observation log-likelihood
method kstep(Particle self, float y_lo, float y_hi, float r_scale, float sig_pop, float vol_ratio)=>
    float y = self.side > 0 ? y_lo : y_hi
    float q_lvl = rho_q * r_scale * vol_ratio
    float q_slp = math.pow(sig_pop / nu_beta, 2)
    float mp0 = self.m0 + self.m1
    float mp1 = self.m1
    float pp00 = self.p00 + 2 * self.p01 + self.p11 + q_lvl
    float pp01 = self.p01 + self.p11
    float pp11 = self.p11 + q_slp
    float s = pp00 + r_scale
    float innov = y - mp0
    float loglike = -0.5 * (math.log(2 * math.pi * s) + innov * innov / s)
    float k0 = pp00 / s
    float k1 = pp01 / s
    self.m0 := mp0 + k0 * innov
    self.m1 := mp1 + k1 * innov
    self.p00 := pp00 - k0 * pp00
    self.p01 := pp01 - k0 * pp01
    self.p11 := pp11 - k1 * pp01
    self.age += 1
    loglike

//@function decays every particle by the hazard and advances it one bar
//@returns the leaked probability mass
method advance(Particle[] self, Tape tape, float h_eff, float r_scale, float sig_pop, float vol_ratio)=>
    float leak = 0.0
    for pt in self
        float ll = pt.kstep(tape.y_lo, tape.y_hi, r_scale, sig_pop, vol_ratio)
        leak += math.exp(pt.logw) * h_eff
        pt.logw += math.log(math.max(1.0 - h_eff, 1e-12)) + ll
    leak

//@function spawns the twin newborns, one per flavor; the trend-direction partition
// splits the leaked mass between them
method spawn(Particle[] self, Stats stats, Tape tape, float leak, float p_up, float ms1, float r_scale, float sig_pop, float vol_ratio)=>
    for int flav = 0 to 1
        int side_f = flav == 0 ? 1 : -1
        float seed = side_f > 0 ? nz(stats.prev_lo, tape.y_lo) : nz(stats.prev_hi, tape.y_hi)
        Particle fresh = Particle.new(stats.next_id, side_f, 0, 0.0, seed, ms1, r_scale * 4.0, 0.0, math.max(sig_pop * sig_pop, 1e-10))
        stats.next_id += 1
        float ll_f = fresh.kstep(tape.y_lo, tape.y_hi, r_scale, sig_pop, vol_ratio)
        fresh.logw := (self.size() > 2 ? math.log(math.max(leak, 1e-300)) : math.log(0.5)) + math.log(side_f > 0 ? p_up : 1.0 - p_up) + ll_f
        self.push(fresh)
    stats.prev_lo := tape.y_lo
    stats.prev_hi := tape.y_hi
    self

//@function renormalizes log-weights into a proper distribution
method renorm(Particle[] self)=>
    float max_lw = -1e300
    for pt in self
        max_lw := math.max(max_lw, pt.logw)
    float z = 0.0
    for pt in self
        z += math.exp(pt.logw - max_lw)
    for pt in self
        pt.logw := pt.logw - max_lw - math.log(math.max(z, 1e-300))
    self

//@function evicts the weakest surplus particles; the two newest are protected
method prune(Particle[] self, int cap)=>
    while self.size() > cap
        int drop = -1
        float worst_lw = 1e300
        for int i = 0 to self.size() - 3
            if self.get(i).logw < worst_lw
                worst_lw := self.get(i).logw
                drop := i
        if drop >= 0
            self.remove(drop)
        else
            break
    self

//@function posterior mass held by one flavor
method mass(Particle[] self, int side)=>
    float m = 0.0
    for pt in self
        if pt.side == side
            m += math.exp(pt.logw)
    m

//@function the highest-weight particle of one flavor; na when the flavor is empty
method map_of(Particle[] self, int side)=>
    Particle best = na
    for pt in self
        if pt.side == side and (na(best) or pt.logw > best.logw)
            best := pt
    best

//@function the flavor's own slope posterior (weighted mean/sd over mature particles) and
// its run agreement (mass within 3 bars of the MAP run age). The swarm has already
// tracked the live regime, so its slope belief is an alternative angle authority to the
// locked-line history - a steep run the swarm believes in is a valid line.
//@returns [slope mean, slope sd (floored), agreement]
method posture(Particle[] self, int side, float floor)=>
    Particle mp = self.map_of(side)
    if na(mp)
        [0.0, 0.5 * floor, 0.0]
    else
        float agree = 0.0
        float wsum = 0.0
        float wmu = 0.0
        for pt in self
            if pt.side == side
                float w = math.exp(pt.logw)
                if math.abs(pt.age - mp.age) <= 3
                    agree += w
                if pt.age >= 3
                    wsum += w
                    wmu += w * pt.m1
        float mu = wsum > 0 ? wmu / wsum : mp.m1
        float sd = 0.5 * floor          // no mature particles yet: the wide prior
        if wsum > 0
            float vr = 0.0
            for pt in self
                if pt.side == side and pt.age >= 3
                    float w = math.exp(pt.logw)
                    vr += w * math.pow(pt.m1 - mu, 2)
            sd := math.max(math.sqrt(math.max(vr / wsum, 0.0)), 0.05 * floor)
        [mu, sd, agree]

// swarm }

// stats {

//@function updates the volatility and volume trackers
//@returns the bar's volume ratio against its own average
method note_bar(Stats self)=>
    float tr = math.max(high - low, syminfo.mintick)
    self.atr := na(self.atr) ? tr : self.atr + atr_alpha * (tr - self.atr)
    self.atr_fast := na(self.atr_fast) ? tr : self.atr_fast + atr_fast_a * (tr - self.atr_fast)
    float v_bar = math.max(nz(volume), 1e-9)
    self.vbar := na(self.vbar) ? v_bar : self.vbar + atr_alpha * (v_bar - self.vbar)
    self.vr := clamp(v_bar / math.max(self.vbar, 1e-9), 0.25, 8.0)
    self.vr

//@function the dual-timescale geometry unit: slow ATR widened toward the fast ATR during
// a volatility spike, so stillness/tangency normalizations do not lag the regime (the
// steep-rally capture fix - the lagging slow ATR starved locks during vertical runs)
method unit(Stats self)=> na(self.atr) ? 1.0 : self.atr * clamp(nz(self.atr_fast, self.atr) / math.max(self.atr, 1e-9), 1.0, 3.0)

//@function adapts the observation-noise multiplier so innovations match their predicted
// scale (NIS matching)
method tune_r(Stats self, float innov, float s)=>
    if s > 0 and bar_index > 10
        float nis = innov * innov / s
        self.r_mult := clamp(self.r_mult * math.exp(0.05 * (math.min(nis, 9.0) - 1.0)), 0.25, 16.0)
    self

//@function spread of the locked-slope population, floored ATR-relative: near-identical
// locked slopes would collapse the raw sd toward zero and freeze the engine
method sigma_pop(Stats self)=> self.ang_n < 4 ? 0.5 * nz(self.atr, 1.0) : math.max(math.sqrt(self.ang_m2 / (self.ang_n - 1)), 0.05 * nz(self.atr, 1.0))

//@function mean of the locked-slope population; zero until it has enough members
method ang_mu(Stats self)=> self.ang_n >= 4 ? self.ang_mean : 0.0

//@function admits one locked slope into the population (Welford update)
method note_angle(Stats self, float b)=>
    self.ang_n += 1
    float d = b - self.ang_mean
    self.ang_mean += d / self.ang_n
    self.ang_m2 += d * (b - self.ang_mean)
    self

// stats }

// perceptual {

//@function the perceptual score: rewards tangency clusters and span; penalizes body-cut
// volume, off-price gap runs, and angles implausible against the learned slope
// population (judged against its mean, not zero - with-trend steepness is normal)
//@returns [score, tangency cluster count]
method pscore(Tape self, float a_, float b_, int anchor_t, int side, int t0, int t1, float sig_pop, float ang_mu, float ang_mu2 = na, float sig2 = na)=>
    float[] hq_ring = self.hug(side)
    int touches = 0
    int last_touch = -10
    float cut = 0.0
    int gap_run = 0
    int worst = 0
    int n = 0
    for int t = t0 to t1
        float hq = hq_ring.ring_at(t)
        // tangency normalizes by the dual-timescale unit: a vol spike widens the
        // touch band immediately instead of after the slow ATR catches up
        float u_t = nz(self.unit.ring_at(t), self.atr.ring_at(t))
        if not na(hq) and not na(u_t)
            float line_y = a_ + b_ * (t - anchor_t)
            float gap = (hq - line_y) * side / math.max(u_t, 1e-9)
            if math.abs(gap) <= eps_touch
                if t - last_touch >= touch_sep
                    touches += 1
                last_touch := t
            if gap > 1.5
                gap_run += 1
                worst := math.max(worst, gap_run)
            else
                gap_run := 0
            cut += self.pen(t, line_y, side)
            n += 1
    float score = -1e9
    if n > 0
        cut /= n
        // angle plausibility against the learned population, OR the swarm's own live
        // slope belief when supplied - the smaller z wins (a steep run the swarm tracks
        // is not penalized against a history of shallow locks)
        float ang_z = math.abs(b_ - ang_mu) / math.max(sig_pop, 1e-12)
        if not na(ang_mu2)
            ang_z := math.min(ang_z, math.abs(b_ - ang_mu2) / math.max(sig2, 1e-12))
        score := w_touch * touches + w_span * math.log(1 + t1 - t0) - w_cut * math.max(0.0, cut - tau) - w_gap * worst - w_ang * math.max(0.0, ang_z - 2.0)
    [score, touches]

//@function recent hug-side swing extrema paired into candidate lines (pivots propose
// candidates; they are never the estimator)
//@returns [intercepts, slopes, anchor bars]
method swing_candidates(Tape self, int side, int look = 42, int keep = 5)=>
    float[] hq_ring = self.hug(side)
    float[] cand_a = array.new<float>()
    float[] cand_b = array.new<float>()
    int[] cand_t = array.new<int>()
    int size = hq_ring.size()
    int lo = math.max(size - look, 2)
    int[] ext_t = array.new<int>()
    float[] ext_y = array.new<float>()
    for int i = lo to size - 3
        float v = hq_ring.get(i)
        bool is_ext = true
        for int j = i - 2 to i + 2
            if side > 0 ? hq_ring.get(j) < v : hq_ring.get(j) > v
                is_ext := false
                break
        if is_ext
            ext_t.push(bar_index - (size - 1 - i))
            ext_y.push(v)
    while ext_t.size() > keep
        ext_t.shift()
        ext_y.shift()
    if ext_t.size() >= 2
        for int i = 0 to ext_t.size() - 2
            for int j = i + 1 to ext_t.size() - 1
                if ext_t.get(j) - ext_t.get(i) >= touch_sep
                    cand_a.push(ext_y.get(i))
                    cand_b.push((ext_y.get(j) - ext_y.get(i)) / (ext_t.get(j) - ext_t.get(i)))
                    cand_t.push(ext_t.get(i))
    [cand_a, cand_b, cand_t]

//@function one-sided volume-weighted quantile trend fit: coordinate descent between the
// intercept and slope quantile solves, with a bounded prior toward the anchor
//@returns [intercept at the anchor, slope]
method trend_fit(Tape self, int anchor_t, float anchor_y, float prior_b, int side, int fit_cap)=>
    int span = bar_index - anchor_t
    int use = math.min(span, math.min(fit_cap, self.bars_held() - 1))
    float a = anchor_y
    float b = prior_b
    if use >= 1
        float q = side > 0 ? tau : 1.0 - tau
        float tail = math.min(q, 1.0 - q)
        int size = self.prints_p.size()
        float[] resid = array.new<float>()
        float[] w_flat = array.new<float>()
        float[] u_slope = array.new<float>()
        float[] w_lev = array.new<float>()
        float[] d_of = array.new<float>()
        for int j = 0 to use - 1
            float d = span - j
            int base = size - (j + 1) * self.slots
            for int k = 0 to self.slots - 1
                resid.push(self.prints_p.get(base + k))
                w_flat.push(self.prints_v.get(base + k))
                u_slope.push(self.prints_p.get(base + k))
                w_lev.push(self.prints_v.get(base + k) * math.pow(d, gamma_lev))
                d_of.push(d)
        float w_sum = 0.0
        for wv in w_flat
            w_sum += wv
        float w_mean = w_sum / math.max(w_flat.size(), 1)
        float wl_sum = 0.0
        for wv in w_lev
            wl_sum += wv
        float wl_mean = wl_sum / math.max(w_lev.size(), 1)
        float pseudo_a = math.min(lam * self.slots * w_mean, 0.5 * tail * w_sum)
        float pseudo_b = math.min(lam * self.slots * wl_mean, 0.5 * tail * wl_sum)
        for int pass_i = 1 to 2
            float[] rv = array.new<float>()
            for int i = 0 to resid.size() - 1
                rv.push(resid.get(i) - b * d_of.get(i))
            rv.push(anchor_y)
            float[] rw = w_flat.copy()
            rw.push(pseudo_a)
            a := wq(rv, rw, q)
            float[] uv = array.new<float>()
            for int i = 0 to u_slope.size() - 1
                uv.push((u_slope.get(i) - a) / d_of.get(i))
            uv.push(prior_b)
            float[] uw = w_lev.copy()
            uw.push(pseudo_b)
            b := wq(uv, uw, q)
    [a, b]

//@function the anchor: hug-side body-quantile extreme over a run's opening bars. The
// proposed origin clamps into the held tape first - a long-lived particle can propose
// an origin older than the tape, which would invert the scan loop (Pine for-loops
// descend when the bounds invert) and waste a pass over dead indices.
//@returns [anchor bar, anchor price]
method find_anchor(Tape self, int origin, int side)=>
    int oldest = bar_index - self.bars_held() + 1
    int origin_c = math.max(oldest, math.min(origin, bar_index))
    int lo_t = math.max(origin_c - 1, oldest)
    int hi_t = math.min(origin_c + 4, bar_index)
    float qa = side > 0 ? tau_anchor : 1.0 - tau_anchor
    int best_t = lo_t
    float best_y = na
    int size = self.prints_p.size()
    for int t = lo_t to hi_t
        int base = size - (bar_index - t + 1) * self.slots
        if base >= 0
            float[] pv = array.new<float>()
            float[] vv = array.new<float>()
            for int k = 0 to self.slots - 1
                pv.push(self.prints_p.get(base + k))
                vv.push(self.prints_v.get(base + k))
            float yq = wq(pv, vv, qa)
            if na(best_y) or (side > 0 ? yq < best_y : yq > best_y)
                best_t := t
                best_y := yq
    [best_t, nz(best_y, close)]

// perceptual }

// lines {

//@function the line's price at bar t: displayed geometry while forming, frozen after
method at(AsLine self, int t)=>
    self.state == 0 ? self.a_disp + self.b_disp * (t - self.anchor_t) : self.a + self.b * (t - self.anchor_t)

//@function deletes every drawing the line owns
method wipe(AsLine self)=>
    while self.strokes.size() > 0
        self.strokes.pop().delete()
    while self.tags.size() > 0
        self.tags.pop().delete()
    if false
        log.info("void")

//@function renders the line as its story: solid from anchor to the break (its locked
// span), a short dashed stub past the break, a lock dot and a break x; sketches are
// dotted and alive. Glow tracks survival; dim retires archived ink.
method paint(AsLine self, Config config, float dim = 1.0)=>
    self.wipe()
    int x1 = self.end_bar
    int x0 = self.anchor_t
    if x1 > x0
        color base = self.side > 0 ? config.bull_color : config.bear_color
        if self.state == 0
            self.strokes.push(line.new(x0, self.at(x0), x1, self.at(x1), color = base.new_alpha(55.0 + 30.0 * (1.0 - dim)), style = line.style_dotted, width = 1))
        else
            int solid_end = self.break_t >= 0 ? math.min(self.break_t, x1) : x1
            float glow = clamp(self.p_alive, 0.15, 1.0) * dim
            self.strokes.push(line.new(x0, self.at(x0), solid_end, self.at(solid_end), color = base.new_alpha(100.0 - 90.0 * dim), width = 2))
            if self.state == 1 and dim >= 1.0
                self.strokes.push(line.new(x0, self.at(x0), solid_end, self.at(solid_end), color = base.new_alpha(88), width = math.max(1, int(math.round(6 * glow)))))
                self.strokes.push(line.new(x0, self.at(x0), solid_end, self.at(solid_end), color = base.new_alpha(80), width = math.max(1, int(math.round(3 * glow)))))
            if self.lock_t >= 0
                self.tags.push(label.new(self.lock_t, self.at(self.lock_t), "◉", color = #00000000, textcolor = base.new_alpha(100.0 - 90.0 * dim), style = label.style_label_center, size = size.tiny))
            if self.break_t >= 0
                if x1 > solid_end
                    self.strokes.push(line.new(solid_end, self.at(solid_end), x1, self.at(x1), color = base.new_alpha(100.0 - 60.0 * dim), style = line.style_dashed, width = 1))
                self.tags.push(label.new(self.break_t, self.at(self.break_t), "✕", color = #00000000, textcolor = base.new_alpha(100.0 - 90.0 * dim), style = label.style_label_center, size = size.small))
    if false
        log.info("void")

//@function the audit badge for locked ink: recent wick-through (an exponentially
// weighted rate, not a lifetime total) versus the design target, survival probability,
// and maturity - run length so far over the predicted total run
badge_text(AsLine ink, float surv_h)=>
    float span_b = bar_index - ink.anchor_t
    float mat = span_b / math.max(span_b + surv_len(ink.p_alive, surv_h), 1.0)
    "recent wick " + str.tostring(ink.pen_avg * 100, "#.#") + "% (target " + str.tostring(tau * 100, "#") + "%) · survival " + str.tostring(ink.p_alive * 100, "#") + "% · maturity " + str.tostring(mat * 100, "#") + "%"

// lines }

// channel {

//@function survival audit and break test for the channel's locked ink; broken ink keeps
// a short dashed tail past its break node
method audit(Channel self, Config config, Tape tape, Stats stats)=>
    if not na(self.ink) and self.ink.state == 1
        AsLine ink = self.ink
        ink.end_bar := bar_index
        float line_y = ink.at(bar_index)
        float pen = tape.pen(bar_index, line_y, self.side)
        ink.pen_avg += 0.2 * (pen - ink.pen_avg)
        float out_gap = math.max(0.0, (tape.hug_now(self.side) - line_y) * self.side / math.max(stats.unit(), 1e-9) - 2.5)
        // learned discrete-time break hazard (survival head) replaces the hand-tuned
        // evidence decay: out_gap on the dual-ATR unit, hug distance on the raw ATR
        float gap_hug = math.abs((tape.hug_now(self.side) - line_y) * self.side / math.max(stats.atr, 1e-9))
        float h_k = surv_hazard(bar_index - ink.lock_t, pen, out_gap, gap_hug, stats.vr)
        ink.p_alive := clamp(ink.p_alive * (1.0 - h_k), 0.02, 0.995)
        bool close_through = (line_y - tape.y_mid) * self.side / math.max(stats.unit(), 1e-9) > break_close
        self.break_run := close_through ? self.break_run + 1 : 0
        if self.break_run >= config.break_bars or ink.p_alive < 0.15
            ink.state := 2
            ink.break_t := bar_index
            self.break_run := 0
    if not na(self.ink) and self.ink.state == 2
        self.ink.end_bar := math.min(bar_index, self.ink.break_t + 5)
    if false
        log.info("void")

//@function births a sketch when the channel has no live line - the channel is never
// empty; the flavor's best particle proposes the origin
method hatch(Channel self, Particle[] pool, Tape tape)=>
    if na(self.sketch) and (na(self.ink) or self.ink.state == 2)
        Particle mp = pool.map_of(self.side)
        int origin = na(mp) ? math.max(bar_index - 4, 0) : math.max(bar_index - mp.age + 1, 0)
        [t0a, y0a] = tape.find_anchor(origin, self.side)
        float pb = na(mp) ? 0.0 : mp.m1
        self.sketch := AsLine.new(self.side, 0, t0a, y0a, y0a, pb, y0a, pb, pb, bar_index, end_bar = bar_index, strokes = array.new<line>(), tags = array.new<label>())
    if false
        log.info("void")

//@function revises the sketch's origin from the flavor's best particle - free look-back
// while forming (mutability is the disclosure); intercepts live at anchor_t, so they
// rebase before the frame moves
method relocate(Channel self, Particle[] pool, Tape tape)=>
    if not na(self.sketch)
        AsLine sk = self.sketch
        Particle mp = pool.map_of(self.side)
        if not na(mp)
            int origin = math.max(bar_index - mp.age + 1, 0)
            if math.abs(origin - sk.anchor_t) > 3 and bar_index - origin >= 3
                [t0r, y0r] = tape.find_anchor(origin, self.side)
                if t0r != sk.anchor_t
                    float shift_r = t0r - sk.anchor_t
                    sk.a += sk.b * shift_r
                    sk.a_disp += sk.b_disp * shift_r
                    sk.anchor_t := t0r
                    sk.anchor_y := y0r
                    sk.prior_b := mp.m1
    if false
        log.info("void")

//@function polishes the sketch one bar: the quantile fit is the incumbent, swing pairs
// and local perturbations challenge it, hysteresis keeps the pen steady, and the
// displayed geometry eases toward the winner; stillness accrues toward a lock
method polish(Channel self, Config config, Tape tape, Stats stats, Particle[] pool, float sig_pop)=>
    if not na(self.sketch)
        AsLine sk = self.sketch
        float ang_mu = stats.ang_mu()
        // the flavor's own live slope belief - a second angle authority
        [sw_mu, sw_sd, sw_agree] = pool.posture(self.side, stats.atr)
        float prev_y = sk.a_disp + sk.b_disp * (bar_index - sk.anchor_t)
        [a_fit, b_fit] = tape.trend_fit(sk.anchor_t, sk.anchor_y, sk.prior_b, self.side, config.fit_cap)
        int t0s = math.max(sk.anchor_t, bar_index - config.fit_cap + 1)
        t0s := math.max(t0s, bar_index - tape.hug(self.side).size() + 1)
        [inc_s, inc_touch] = tape.pscore(a_fit, b_fit, sk.anchor_t, self.side, t0s, bar_index, sig_pop, ang_mu, sw_mu, sw_sd)
        float best_s = -1e18
        float best_a = na
        float best_b = na
        int best_touch = 0
        [ca, cb, ct] = tape.swing_candidates(self.side)
        if ca.size() > 0  
            for [index, intercept] in ca
                float a_n = intercept + cb.get(index) * (sk.anchor_t - ct.get(index))
                [s_c, tc] = tape.pscore(a_n, cb.get(index), sk.anchor_t, self.side, t0s, bar_index, sig_pop, ang_mu, sw_mu, sw_sd)
                if s_c > best_s
                    best_s := s_c
                    best_a := a_n
                    best_b := cb.get(index)
                    best_touch := tc
        for int pa_i = 0 to 1
            float da = pa_i == 0 ? -0.25 : 0.25
            for int pb_i = 0 to 2
                float db = pb_i == 0 ? -0.08 : pb_i == 1 ? 0.0 : 0.08
                [s_c2, tc2] = tape.pscore(a_fit + da * stats.atr, b_fit + db * sig_pop, sk.anchor_t, self.side, t0s, bar_index, sig_pop, ang_mu, sw_mu, sw_sd)
                if s_c2 > best_s
                    best_s := s_c2
                    best_a := a_fit + da * stats.atr
                    best_b := b_fit + db * sig_pop
                    best_touch := tc2
        if not na(best_a) and best_s > math.max(inc_s * config.p_hyst, inc_s + 0.5)
            sk.a := best_a
            sk.b := best_b
            sk.touches := best_touch
        else
            sk.a := a_fit
            sk.b := b_fit
            sk.touches := inc_touch
        sk.a_disp += config.disp_a * (sk.a - sk.a_disp)
        sk.b_disp += config.disp_a * (sk.b - sk.b_disp)
        sk.end_bar := bar_index
        // stillness in dual-unit terms; overwhelming swarm agreement fast-tracks it (a
        // steep leg's lock window is short - the swarm converging on one run is the tell)
        float jit = math.abs((sk.a_disp + sk.b_disp * (bar_index - sk.anchor_t)) - prev_y) / math.max(stats.unit(), 1e-9)
        if jit <= config.lock_jit
            int credit = sw_agree >= swarm_fast and bar_index - sk.anchor_t >= 6 and sk.touches >= lock_min_touch ? 2 : 1
            sk.still += credit
        else
            sk.still := 0
    if false
        log.info("void")

//@function one-time look-back at lock: walk the just-frozen geometry backward,
// claiming earlier bars it already describes (break volume <= retro_pen and hug-gap
// within tolerance; two violations stop the walk, capped at retro_max). Re-anchor,
// refit, and keep the extension only if the lock's tangency and angle survive.
method retro_extend(AsLine sk, Config config, Tape tape, Stats stats, float sig_pop, float ang_mu, float ang_mu2, float sig2)=>
    int best = sk.anchor_t
    int viol = 0
    int lo_lim = math.max(sk.anchor_t - retro_max, 1)
    if sk.anchor_t - 1 >= lo_lim
        for int s = sk.anchor_t - 1 to lo_lim
            float line_y = sk.a + sk.b * (s - sk.anchor_t)
            float p = tape.pen(s, line_y, sk.side)
            float hq = tape.hug(sk.side).ring_at(s)
            float a_s = nz(tape.atr.ring_at(s), stats.atr)
            float gap = na(hq) ? 99.0 : (hq - line_y) * sk.side / math.max(a_s, 1e-9)
            if p <= retro_pen and gap <= 1.2
                best := s
                viol := 0
            else
                viol += 1
                if viol >= 2
                    break
    if best < sk.anchor_t
        [t0r, y0r] = tape.find_anchor(best, sk.side)
        if t0r < sk.anchor_t
            [a_n, b_n] = tape.trend_fit(t0r, y0r, sk.b, sk.side, config.fit_cap)
            int t0 = math.max(t0r, bar_index - config.fit_cap + 1)
            [s_n, touches_n] = tape.pscore(a_n, b_n, t0r, sk.side, t0, bar_index, sig_pop, ang_mu, ang_mu2, sig2)
            bool ang_ok = math.abs(b_n - ang_mu) <= ang_max_z * sig_pop or (not na(ang_mu2) and math.abs(b_n - ang_mu2) <= ang_max_z * sig2)
            if touches_n >= lock_min_touch and ang_ok
                sk.anchor_t := t0r
                sk.anchor_y := y0r
                sk.a := a_n
                sk.b := b_n
                sk.a_disp := a_n
                sk.b_disp := b_n
                sk.touches := touches_n
    sk

//@function would locking this sketch invert the channel against the opposite side's
// LOCKED ink, now or within h_cross bars? Only frozen ink constrains - a mutable forming
// sketch is ignored (reacting to a mutable line delays good locks and raises crossings).
method crosses_locked(Channel self, AsLine other, AsLine sk)=>
    bool bad = false
    if not na(other) and other.state == 1
        for int k = 0 to h_cross
            float here = sk.a_disp + sk.b_disp * (bar_index + k - sk.anchor_t)
            float there = other.at(bar_index + k)
            if (self.side > 0 ? here >= there : here <= there)
                bad := true
                break
    bad

//@function locks the sketch once its geometry stills and it has earned tangency, swarm,
// and angle credit; the broken predecessor retires to the archive
method try_lock(Channel self, Config config, Tape tape, Stats stats, Particle[] pool, AsLine other, float sig_pop)=>
    if not na(self.sketch)
        AsLine sk = self.sketch
        [sw_mu, sw_sd, sw_agree] = pool.posture(self.side, stats.atr)
        // angle valid against the learned population OR the swarm's live belief
        bool angle_ok = math.abs(sk.b_disp - stats.ang_mu()) <= ang_max_z * sig_pop or math.abs(sk.b_disp - sw_mu) <= ang_max_z * sw_sd
        if sk.still >= config.lock_k and bar_index - sk.anchor_t >= config.lock_min_span and sk.touches >= lock_min_touch and pool.mass(self.side) >= lock_swarm and angle_ok and not self.crosses_locked(other, sk)
            sk.state := 1
            sk.a := sk.a_disp
            sk.b := sk.b_disp
            sk.lock_t := bar_index
            sk.p_alive := 0.95
            sk.retro_extend(config, tape, stats, sig_pop, stats.ang_mu(), sw_mu, sw_sd)
            stats.note_angle(sk.b)
            if not na(self.ink)
                if config.show_archive and (self.side > 0 ? config.show_sup : config.show_res)
                    self.ink.paint(config, 0.35)
                    self.archive.push(self.ink)
                    if self.archive.size() > archive_keep
                        self.archive.shift().wipe()
                else
                    self.ink.wipe()
            self.ink := sk
            self.sketch := na
    if false
        log.info("void")

// channel }

// forecast {

//@function deletes the previous render's forecast drawings
method wipe(Forecast self)=>
    while self.polys.size() > 0
        self.polys.pop().delete()
    while self.levels.size() > 0
        self.levels.pop().delete()
    if false
        log.info("void")

//@function cross-trend residual volume histogram over the element's span; residuals
// outside the span are dropped, not clamped - clamping piles phantom mass into the
// edge bins
method residual_hist(Tape self, AsLine tgt, int fit_cap, float atr_unit)=>
    float[] hist = array.new<float>(pred_bins, 0.0)
    int t0h = math.max(tgt.anchor_t + 1, bar_index - fit_cap + 1)
    int csize = self.prints_p.size()
    for int t = t0h to bar_index
        int base_i = csize - (bar_index - t + 1) * self.slots
        if base_i >= 0
            float line_y = tgt.at(t)
            float atr_t = nz(self.atr.ring_at(t), atr_unit)
            for int k = 0 to self.slots - 1
                float rr = (self.prints_p.get(base_i + k) - line_y) / math.max(atr_t, 1e-9)
                if math.abs(rr) <= pred_span
                    int bi = int(clamp((rr + pred_span) / (2 * pred_span) * (pred_bins - 1), 0, pred_bins - 1))
                    hist.set(bi, hist.get(bi) + self.prints_v.get(base_i + k))
    hist

//@function gaussian smoothing over the residual bins (a binned kernel density estimate):
// turns the coarse histogram into a smooth, high-resolution density so the band gradient
// reads continuous rather than stepped. Edge-normalized so the tails do not darken.
method kde(float[] src)=>
    int n = src.size()
    float[] out = array.new<float>(n, 0.0)
    for int i = 0 to n - 1
        float acc = 0.0
        float wsum = 0.0
        for int d = -kde_radius to kde_radius
            int j = i + d
            if j >= 0 and j < n
                float wk = math.exp(-0.5 * (d / kde_sigma) * (d / kde_sigma))
                acc += src.get(j) * wk
                wsum += wk
        out.set(i, acc / math.max(wsum, 1e-9))
    out

//@function the calibration band: one translucent ribbon per occupied residual bin, from
// the element's start to now, riding parallel to the line; overlaps simply alpha-blend.
// It is the aggregate residual profile extruded along the line at the current
// volatility scale - where the trend has held its volume, not a pointwise replay.
method band(Forecast self, Config config, AsLine tgt, float[] hist, float peak, int x0b, int x1b, float bin_h, float atr_unit, color base)=>
    Ok hue = to_ok(base)
    for int i = 0 to pred_bins - 1
        float dens = hist.get(i) / peak
        if dens >= 0.10
            float off = bin_off(i, atr_unit)
            // vibrancy encodes strength - dense bins vivid, thin tails muted - at constant Oklab lightness
            color c = hue.roll(0.0, 0.0, 0.45 + 0.55 * dens).col()
            chart.point[] rib = array.new<chart.point>()
            rib.push(chart.point.new(na, x0b, tgt.at(x0b) + off - bin_h / 2))
            rib.push(chart.point.new(na, x1b, tgt.at(x1b) + off - bin_h / 2))
            rib.push(chart.point.new(na, x1b, tgt.at(x1b) + off + bin_h / 2))
            rib.push(chart.point.new(na, x0b, tgt.at(x0b) + off + bin_h / 2))
            self.polys.push(polyline.new(rib, curved = false, closed = true, xloc = xloc.bar_index, line_color = na, fill_color = color.new(c, contrast_alpha(100.0 - 14.0 * dens, config.contrast))))
    if false
        log.info("void")

//@function the forecast profile: one filled glowing smooth-contour polyline fanning forward,
// bars parallel to the trend; each bin's reach = its density x the predicted length
// (confidence already shaped the length through the survival start point, so it must
// not scale the reach a second time). The envelope threads one point per occupied bin at
// its center and is splined (curved) into a smooth contour rather than a stepped staircase.
method profile(Forecast self, Config config, AsLine tgt, float[] hist, float peak, int k_len, int x_base, float atr_unit, color base)=>
    int i_min = -1
    int i_max = -1
    for int i = 0 to pred_bins - 1
        if hist.get(i) / peak >= 0.06
            i_max := i
            if i_min < 0
                i_min := i
    if i_min >= 0
        chart.point[] prof = array.new<chart.point>()
        prof.push(chart.point.new(na, x_base, tgt.at(x_base) + bin_off(i_min, atr_unit)))
        for int i = i_min to i_max
            float dens = hist.get(i) / peak
            float off = bin_off(i, atr_unit)
            int reach = math.max(int(math.round(dens * k_len)), 0)
            prof.push(chart.point.new(na, x_base + reach, tgt.at(x_base + reach) + off))
        prof.push(chart.point.new(na, x_base, tgt.at(x_base) + bin_off(i_max, atr_unit)))
        // a single clean contour: crisp vivid outline + translucent fill, no halo glow (Oklab-vivid so
        // the colour reads rich, and its chroma gives the shape definition against the chart)
        color vivid = to_ok(base).roll(0.0, 0.0, 1.15).col()
        self.polys.push(polyline.new(prof, curved = true, closed = true, xloc = xloc.bar_index, line_color = color.new(vivid, contrast_alpha(42, config.contrast)), fill_color = color.new(vivid, contrast_alpha(86, config.contrast)), line_style = line.style_solid, line_width = 2))
    if false
        log.info("void")

//@function ghost levels: the distribution's densest peaks (up to six, strongest first),
// each riding parallel to the beam across the whole element - back to the band's
// start, forward to the predicted length
method ghosts(Forecast self, Config config, AsLine tgt, float[] hist, float peak, int k_len, int x0b, int x_base, float atr_unit, color base)=>
    int[] gi = array.new<int>()
    float[] gd = array.new<float>()
    for int i = 1 to pred_bins - 2
        float dens = hist.get(i) / peak
        if dens >= 0.25 and hist.get(i) >= hist.get(i - 1) and hist.get(i) >= hist.get(i + 1)
            gi.push(i)
            gd.push(dens)
    int drawn = 0
    while drawn < 6 and gd.size() > 0
        int pick = 0
        for int j = 0 to gd.size() - 1
            if gd.get(j) > gd.get(pick)
                pick := j
        float dens = gd.get(pick)
        float off = bin_off(gi.get(pick), atr_unit)
        gi.remove(pick)
        gd.remove(pick)
        color c = to_ok(base).roll(0.0, 0.0, 0.5 + 0.5 * dens).col()   // stronger peaks ride a more vivid ghost line
        self.levels.push(line.new(x0b, tgt.at(x0b) + off, x_base + k_len, tgt.at(x_base + k_len) + off, color = color.new(c, contrast_alpha(100.0 - 35.0 * dens, config.contrast)), style = line.style_dashed, width = 1))
        drawn += 1
    if false
        log.info("void")

//@function forecast conviction: how much this element deserves the single-trend spotlight -
// survival x maturity for locked ink, a lower earned score for a forming sketch, so the
// projection rides the most established, most-alive trend and the other rail stays a bare beam
method conviction(AsLine self, Config config)=>
    if self.state == 1
        float span_b = bar_index - self.anchor_t
        float mat = span_b / math.max(span_b + surv_len(self.p_alive, config.surv_h), 1.0)
        self.p_alive * (0.3 + 0.7 * mat)
    else
        0.5 * clamp(0.45 + 0.06 * self.touches + 0.03 * self.still, 0.45, 0.90)

//@function draws one element's forecast: the calibration band, the reach profile, and
// the ghost levels; the profile sits clear of price, out of its way
method project(Forecast self, Config config, AsLine tgt, Tape tape, float atr_unit)=>
    float[] hist = tape.residual_hist(tgt, config.fit_cap, atr_unit).kde()
    float peak = 0.0
    for hv in hist
        peak := math.max(peak, hv)
    if peak > 0
        color base = tgt.side > 0 ? config.bull_color : config.bear_color
        // forecast confidence: earned for sketches, survival for ink
        float p0 = tgt.state == 1 ? tgt.p_alive : clamp(0.45 + 0.06 * tgt.touches + 0.03 * tgt.still, 0.45, 0.90)
        // geometry draws the capped horizon; the badge reports the uncapped estimate
        int k_len = math.min(surv_len(p0, config.surv_h), pred_kmax)
        float bin_h = 2.0 * pred_span * atr_unit / (pred_bins - 1)
        int x0b = math.max(tgt.anchor_t, bar_index - config.fit_cap + 1)
        // the band rides to the live bar; the profile clears price by a bar so it never
        // draws over the candles
        int x_now = bar_index + 1
        int x_base = bar_index + 2
        self.band(config, tgt, hist, peak, x0b, x_now, bin_h, atr_unit, base)
        self.profile(config, tgt, hist, peak, k_len, x_base, atr_unit, base)
        self.ghosts(config, tgt, hist, peak, k_len, x0b, x_base, atr_unit, base)
    if false
        log.info("void")

// forecast }

// engine {

//@function one bar of observation: volatility and volume telemetry, then the tape rings
//@returns the bar's volume ratio
method observe(Engine self, float[] bar_p, float[] bar_v)=>
    float vol_ratio = self.stats.note_bar()
    self.tape.ingest(bar_p, bar_v, self.stats.atr, self.stats.unit(), self.config.fit_cap + 8)
    vol_ratio

//@function evolves the shared swarm one bar: locked-ink break evidence kicks the hazard
// (capped), every particle decays and advances, the twin newborns split the leaked
// mass by trend direction, and the pool renormalizes and prunes
method evolve(Engine self, float vol_ratio)=>
    float r_scale = math.pow(0.4 * self.stats.atr, 2) * self.stats.r_mult
    [innov_s, s_s] = self.smoother.step(self.tape.y_mid, r_scale, d_lag)
    self.stats.tune_r(innov_s, s_s)
    [ms0, ms1] = self.smoother.smoothed()
    float sig_pop = self.stats.sigma_pop()
    float kick = 0.0
    for chside in array.from(1, -1)
        AsLine ink_c = chside > 0 ? self.sup.ink : self.res.ink
        if not na(ink_c) and ink_c.state == 1
            kick += math.min(kv_kick * math.max(0.0, self.tape.pen(bar_index, ink_c.at(bar_index), chside) - tau) / tau, 0.6)
    float h_eff = 1.0 - (1.0 - self.config.pool_h) * math.exp(-math.min(kick, 1.2))
    float leak = self.pool.advance(self.tape, h_eff, r_scale, sig_pop, vol_ratio)
    float p_up = 0.5 + 0.2 * tanh(ms1 / math.max(0.3 * sig_pop, 1e-12))
    self.pool.spawn(self.stats, self.tape, leak, p_up, ms1, r_scale, sig_pop, vol_ratio)
    self.pool.renorm()
    self.pool.prune(k_particles)
    if false
        log.info("void")

//@function advances both channels one confirmed bar: audit the ink, hatch and relocate
// the sketch, polish its geometry, and try the lock. sigma_pop is snapshotted once per
// bar so a same-bar support lock cannot rescale the resistance channel's scoring; the
// angle mean deliberately reads live, matching the calibrated reference engine.
method assist(Engine self)=>
    float sig_pop = self.stats.sigma_pop()
    for chside in array.from(1, -1)
        Channel ch = chside > 0 ? self.sup : self.res
        AsLine other = chside > 0 ? self.res.ink : self.sup.ink
        ch.audit(self.config, self.tape, self.stats)
        ch.hatch(self.pool, self.tape)
        ch.relocate(self.pool, self.tape)
        ch.polish(self.config, self.tape, self.stats, self.pool, sig_pop)
        ch.try_lock(self.config, self.tape, self.stats, self.pool, other, sig_pop)
    if false
        log.info("void")

//@function renders the duo and the live-edge forecast; forecasts attach only to a
// channel's visible live element and keep no history
method render(Engine self)=>
    Config config = self.config
    self.cast.wipe()
    for chside in array.from(1, -1)
        Channel ch = chside > 0 ? self.sup : self.res
        bool side_on = chside > 0 ? config.show_sup : config.show_res
        if side_on and not na(ch.ink)
            ch.ink.paint(config)
            if config.show_badges and ch.ink.state == 1
                ch.ink.tags.push(label.new(ch.ink.end_bar, ch.ink.at(ch.ink.end_bar), badge_text(ch.ink, config.surv_h), color = #00000000, textcolor = config.chrome_color, style = chside > 0 ? label.style_label_upper_right : label.style_label_lower_right, size = size.large))
        if side_on and not na(ch.sketch) and config.show_sketch
            ch.sketch.paint(config)
        // each visible live element carries its OWN forecast - the calibration band, the
        // reach profile, and the ghost levels riding it. A sketch born this bar would
        // invert the span loop, so it forecasts once it has a bar behind it.
        AsLine tgt = not side_on ? na : not na(ch.sketch) ? (config.show_sketch ? ch.sketch : na) : (not na(ch.ink) and ch.ink.state == 1 ? ch.ink : na)
        if config.show_pred and not na(tgt) and tgt.anchor_t < bar_index
            self.cast.project(config, tgt, self.tape, self.stats.atr)
    if false
        log.info("void")

// engine }

// main {

//@function assembles a fresh engine
make_engine(Config config, int slots)=>
    Smoother smoother = Smoother.new(array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>())
    Tape tape = Tape.new(slots, array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>(), array.new<float>())
    Forecast cast = Forecast.new(array.new<polyline>(), array.new<line>())
    Engine.new(config, Stats.new(), smoother, array.new<Particle>(), Channel.new(1, archive = array.new<AsLine>()), Channel.new(-1, archive = array.new<AsLine>()), tape, cast)

//@function one engine step: observe the bar, evolve the swarm, assist both channels on
// confirmed bars, and render at the live edge. Rendering runs on the last confirmed
// historical bar and at each realtime close - ordinary variables roll back on every
// realtime tick, so confirmed-only rendering is what keeps the forecast stable.
main(Config config, float[] bar_p, float[] bar_v, int slots)=>
    var Engine eng = make_engine(config, slots)
    float vol_ratio = eng.observe(bar_p, bar_v)
    eng.evolve(vol_ratio)
    if bar_index >= warmup and barstate.isconfirmed
        eng.assist()
    if barstate.islastconfirmedhistory or (barstate.islast and barstate.isconfirmed)
        eng.render()
    if false
        log.info("void")

// main }

// HEAD }


// BODY {

// inputs {

// { engine
var const string engine_group = "Trend Engine"

const string mode_tip = "The tempo. Fast reacts quickly: lines lock, break, and re-form sooner, favoring short swings. Slow is patient: lines demand more evidence and hold through more noise, favoring long movements. Medium is the balanced reference tuning."
const string precision_tip = "How much data the engine reads: the perceptual fit window and the intrabar sample rate. Higher tiers resolve finer structure at the cost of load time. Sampling is timeframe-aware - bigger chart bars sample proportionally coarser intrabars, never below one minute."

var Mode tempo = input.enum(Mode.medium, "Mode", tooltip = mode_tip, group = engine_group)
var Preset quality = input.enum(Preset.balanced, "Precision", tooltip = precision_tip, group = engine_group)
// } engine

// { layers
var const string layer_group = "Layers"

const string sketch_tip = "The forming line - the assistant sketching. It moves and shifts freely until it locks; dotted."
const string archive_tip = "Keep broken ink on the chart, dimmed, as history."
const string badge_tip = "The measurement badge on locked ink: recent wick-through (exponentially weighted) versus the 10% design target, current survival probability, and maturity - how far through its predicted total run the trend is."
const string pred_tip = "The live-edge forecast: a translucent calibration band showing where the trend has held its volume, a filled profile whose reach along the trend is the predicted remaining length, and dashed ghost levels at the volume distribution's densest peaks, projected across the whole element. Active lines only - forecasts keep no history."

bool show_sketch = input.bool(true, "Show Forming Lines", tooltip = sketch_tip, group = layer_group)
bool show_archive = input.bool(true, "Show Broken History", tooltip = archive_tip, group = layer_group)
bool show_badges = input.bool(true, "Show Audit Badges", tooltip = badge_tip, group = layer_group)
bool show_pred = input.bool(true, "Show Trend Forecast", tooltip = pred_tip, group = layer_group)
// } layers

// { colors
var const string color_group = "Colors"

color bull_color = input.color(#54aaee, "Support", tooltip = "The support channel: forming, locked, broken.", group = color_group)
color bear_color = input.color(#b051c8, "Resistance", tooltip = "The resistance channel: forming, locked, broken.", group = color_group)
color chrome_color = input.color(#a8bdce, "Chrome", tooltip = "Badges and neutral furniture.", group = color_group)
float contrast = input.float(1.0, "Contrast", minval = 0.4, maxval = 2.0, step = 0.1, tooltip = "Opacity of the translucent forecast layers - the volume band, the profile, and the ghost levels. Higher is bolder.", group = color_group)
// } colors

// { channels
var const string side_group = "Channels"

const string sup_tip = "Show the support channel - its line, badge, and forecast. The engine models both sides either way; hiding one changes nothing about the other."
const string res_tip = "Show the resistance channel - its line, badge, and forecast. The engine models both sides either way; hiding one changes nothing about the other."

bool show_sup = input.bool(true, "Support Channel", tooltip = sup_tip, group = side_group)
bool show_res = input.bool(true, "Resistance Channel", tooltip = res_tip, group = side_group)
// } channels

// the tempo table - behavior only; the precision table below owns how much data is read
int lock_k_m = switch tempo
    Mode.fast => 3
    Mode.slow => 8
    => 5
float lock_jit_m = switch tempo
    Mode.fast => 0.20
    Mode.slow => 0.12
    => 0.16544033202169228
int lock_span_m = switch tempo
    Mode.fast => 7
    Mode.slow => 14
    => 12
int break_bars_m = switch tempo
    Mode.fast => 2
    Mode.slow => 5
    => 4
float pool_h_m = switch tempo
    Mode.fast => 1.0 / 40.0
    Mode.slow => 1.0 / 160.0
    => 1.0 / 80.0
float surv_h_m = switch tempo
    Mode.fast => 1.0 / 25.0
    Mode.slow => 1.0 / 70.0
    => 1.0 / 40.0
float p_hyst_m = switch tempo
    Mode.fast => 1.06
    Mode.slow => 1.18
    => 1.10
float disp_a_m = switch tempo
    Mode.fast => 0.50
    Mode.slow => 0.25
    => 0.35

// the precision table: the fit/score window and the intrabar sample-rate target
// (bounded by the chart timeframe in calculations)
int fit_cap = switch quality
    Preset.fast => 96
    Preset.balanced => 140
    Preset.precision => 200
    Preset.extra => 260
    Preset.max => 320
    => 140
int slots_target = switch quality
    Preset.fast => 6
    Preset.balanced => 10
    Preset.precision => 14
    Preset.extra => 18
    Preset.max => 24
    => 10

var Config config = Config.new(
   fit_cap
 , lock_k_m
 , lock_jit_m
 , lock_span_m
 , break_bars_m
 , pool_h_m
 , surv_h_m
 , p_hyst_m
 , disp_a_m

 , show_sketch
 , show_archive
 , show_badges
 , show_pred
 , show_sup
 , show_res

 , bull_color
 , bear_color
 , chrome_color
 , contrast
 )

// inputs }

// calculations {

int chart_sec = timeframe.in_seconds(timeframe.period)
if na(chart_sec)
    runtime.error("Trend Beams requires a time-based chart.")
if chart_sec < 60
    runtime.error("Trend Beams requires a chart timeframe of one minute or higher (its volume cloud samples intrabar data).")

int chart_min = math.max(nz(chart_sec, 60) / 60, 1)
int slots = math.min(slots_target, chart_min)
int ltf_sec = math.max(60, int(math.ceil(nz(chart_sec, 60) / math.max(float(slots), 1.0))))
[ltf_p, ltf_v] = request.security_lower_tf(syminfo.tickerid, timeframe.from_seconds(ltf_sec), [close, volume])

[bar_p, bar_v] = resample(ltf_p, ltf_v, slots)
main(config, bar_p, bar_v, slots)
plot(na, "void", display = display.none)

// calculations }

// errors {

float has_volume = ta.cum(nz(volume))
if barstate.islast
    if has_volume == 0
        runtime.error("This symbol has no volume data. Trend Beams weighs its fits by traded volume.")

// errors }
// BODY }

Overview

Lines are shown forming, then lock in place and are later restyled as broken history. A tempo mode of fast, medium or slow controls how quickly lines lock and break, and a precision preset sets how much data is read. Options show forming lines, broken history, audit badges and a trend forecast, and the support and resistance channels can be switched on or off separately. Colours and contrast are adjustable.

Details

Published by

ankurksoni on GitHub

Code credit

Trading-IQ, as written in the script's header

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Support & resistance, Trend

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
ModeenumMode.mediumtempo
PrecisionenumPreset.balancedquality
Show Forming Linesbooltrueshow_sketch
Show Broken Historybooltrueshow_archive
Show Audit Badgesbooltrueshow_badges
Show Trend Forecastbooltrueshow_pred
Supportcolor#54aaeebull_color
Resistancecolor#b051c8bear_color
Chromecolor#a8bdcechrome_color
Contrastfloat1.0contrast
Support Channelbooltrueshow_sup
Resistance Channelbooltrueshow_res
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

575 stars

This indicator detects equal highs and lows by comparing prices within user-defined left and right bars using a tolerance percentage. It plots horizontal lines on the price chart for equal highs in one color and lows in another. A screener allows input of mult

Indicator v5 Support & resistanceTrend
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Equal Highs and Lows with Screener

Function - Linear Regression

by ricardosantos79
261 stars

This indicator fits a linear regression through stored pivot highs and lows and draws the resulting channel on the price chart. A function returns predicted values, maximum and minimum deviations, and the average absolute deviation. Confirmed two-bar pivots up

Indicator v5 TrendSupport & resistance
Updated 4 years ago · None stated GitHub repository of Function - Linear Regression

AI Trend

by dearvn
257 stars

AI Trend is an indicator that plots pivot points, trend lines, and support and resistance levels directly on the price chart. It marks high and low pivots based on user-defined length, draws trend lines that check for violations at specified points, and identi

Indicator v5 TrendSupport & resistance
Updated 6 weeks ago · None stated GitHub repository of AI Trend

Ichimoku Cloud

by harryguiacorn
219 stars

This indicator plots Ichimoku Cloud elements on the price chart, including Tenkan-sen, Kijun-sen, Senkou Span A and B lines with optional Chikou Span. It signals trend changes via Tenkan-sen and Kijun-sen crosses and places arrows at Kijun-sen direction shifts

Indicator v5 TrendMoving averages
Updated 1 year ago · None stated GitHub repository of Ichimoku Cloud
219 stars

This indicator identifies local swing highs and swing lows to construct a Zig Zag pattern on the main price chart. It evaluates pivot points based on a customizable number of surrounding left and right bars. Using persistent variables, the script tracks previo

Indicator v4 Support & resistanceTrend Repaints
Updated 5 years ago · None stated GitHub repository of Teaching: Finding Highs and Lows 6

Dynamic Linear Regression

by TradersPost
218 stars

Dynamic Linear Regression calculates and plots linear regression trendlines and standard deviation channels directly on the price chart over a user-selected time period. Traders can configure static channels locked at the end time, dynamic channels that contin

Indicator v6 TrendSupport & resistance
Updated 3 months ago · None stated GitHub repository of Dynamic Linear Regression

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers