Indicator · Pine Script v6 · On the chart

Zen Swing Trendlines

by Zen-Tim

This indicator detects swing highs and lows to plot bull and bear trendlines directly on the main price chart.

View on GitHub

GitHub stars29on the repository
Pine versionv6current
Lines of code291
Settings19inputs
LicenceMIT
Last update2 weeks ago14 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

291 lines of Pine Script v6 from indicators/Zen-Swing-Trendlines/zen-swing-trendlines-v1.2.txt, published under MIT. Keep the author's credit and licence lines when you reuse it.

zen-swing-trendlines-v1.2.txt
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Zen_Tim_Trades
// Zen Swing Trendlines v1.2 · 20260910
// Tim Fairweather · Zen Trading Tech
//
// Joins swing points into trendlines and projects them forward.
//   Bear line: older swing high -> newer, lower swing high.
//   Bull line: older swing low  -> newer, higher swing low.
//
// SWING POINTS — Zen Swing 123 timing (Kaufman, Trading Systems and Methods)
//   A swing flips after N bars in a row move against it (N = 1/2/3/4).
//   2 = Kaufman base method. 1 catches one-bar pullbacks.
//   Anchor = true highest high / lowest low since the prior swing point.
//   (Zen Swing 123 v2.2 starts each leg at the confirm bar instead.)
//
// LINE RULES
//   A line is built only on the bar its second swing point is confirmed.
//   The dot marks that bar. Line segment left of the dot = drawn after the fact.
//   Tolerance = share of ABR a wick or close may pass the line and still count as on it.
//   1. Clean   — no wick between the two anchors passes the line by more than tolerance.
//   2. Intact  — no close through the line (beyond tolerance) from anchor 2 to the confirm bar.
//   3. Slope   — points per bar / ABR <= max steepness. ABR = 8-bar average range, prior bars.
//   4. Ends    — close through the line (broken) or life limit reached (expired). Left faded.
//   5. Tidy    — steepest + shallowest per new swing point; one live line per start point;
//                max live lines per side. Replaced / capped lines end faded (not deleted).
//   6. Live lines hide when price is far away (last bar only).
//
// v1.2  20260910  Swing point triangles and 'appears' dots default off.
// v1.1  20260910  Tolerance now a share of ABR (was ticks) so it scales across timeframes.
//                 Replaced and capped lines end faded instead of being deleted, so every
//                 'appears' dot keeps its line.
// v1.0  20260910  First build.

//@version=6
indicator("Zen Swing Trendlines v1.2", overlay=true, max_lines_count=500, max_labels_count=500)

max_bars_back(high, 1000)
max_bars_back(low, 1000)
max_bars_back(close, 1000)

// ─── INPUTS ───────────────────────────────────────────────────────────────
var string GS = "Swing Points"
i_thr     = input.int(1, "Bars against to confirm", options=[1, 2, 3, 4], group=GS, tooltip="Bars in a row moving against the swing before it flips. 2 = Kaufman base method. 1 catches one-bar pullbacks.")
i_showPiv = input.bool(false, "Show swing points", group=GS)

var string GB = "Line Builder"
i_K        = input.int(5, "Older swing points to try", minval=1, maxval=10, group=GB)
i_maxSpan  = input.int(150, "Max bars between anchors", minval=2, maxval=500, group=GB)
i_maxSteep = input.float(1.0, "Max steepness (ABR per bar)", minval=0.05, step=0.05, group=GB, tooltip="Points the line moves per bar, divided by ABR. Blocks near-vertical lines.")
i_tolAbr   = input.float(0.10, "Tolerance (share of ABR)", minval=0.0, maxval=1.0, step=0.05, group=GB, tooltip="How far a wick or close may pass the line and still count as on it. 0.10 = one tenth of the 8-bar average range.")
i_stSh     = input.bool(true, "Steepest + shallowest only", group=GB, tooltip="When a new swing point pairs with 3+ older points, keep only the nearest (steepest) and farthest (shallowest).")
i_oneOrig  = input.bool(true, "One live line per start point", group=GB, tooltip="A new line from the same start point replaces the older live line.")

var string GR = "Relevance"
i_maxLive = input.int(3, "Max live lines per side", minval=1, maxval=10, group=GR)
i_maxAge  = input.int(78, "Line life (bars after it appears)", minval=5, maxval=1000, group=GR)
i_maxDist = input.float(4.0, "Hide live line beyond (ABR from price)", minval=0.5, step=0.5, group=GR)
i_today   = input.bool(false, "Today's session only", group=GR, tooltip="Clears lines and swing points at the RTH open. Off = lines carry across sessions.")

var string GV = "Visuals"
i_proj     = input.int(10, "Project live lines forward (bars)", minval=0, maxval=200, group=GV)
i_colBear  = input.color(#d1242f, "Bear line", inline="col", group=GV)
i_colBull  = input.color(#1f6feb, "Bull line", inline="col", group=GV)
i_width    = input.int(1, "Line width", minval=1, maxval=4, group=GV)
i_showEnd  = input.bool(true, "Show broken / expired lines", group=GV)
i_endTr    = input.int(70, "Broken / expired transparency", minval=0, maxval=95, group=GV)
i_showBorn = input.bool(false, "Mark bar where line appears", group=GV)

// ─── TYPES ────────────────────────────────────────────────────────────────
type Piv
    int   bar
    float price

type TL
    int   side   // 1 = bear (swing highs), -1 = bull (swing lows)
    int   x1
    float y1
    float m
    int   born
    line  ln

// ─── STATE ────────────────────────────────────────────────────────────────
var array<Piv> hiPiv = array.new<Piv>()
var array<Piv> loPiv = array.new<Piv>()
var array<TL>  bear  = array.new<TL>()
var array<TL>  bull  = array.new<TL>()

var int   sDir    = 0
var float sHi     = na
var float sLo     = na
var int   rc      = 0
var int   lastPiv = na
var int   sessBar = 0

abr = ta.sma(high - low, 8)[1]
tol = na(abr) ? 0.0 : i_tolAbr * abr

// ─── HELPERS ──────────────────────────────────────────────────────────────
f_y(TL t, int x) =>
    t.y1 + t.m * (x - t.x1)

f_col(int side) =>
    side == 1 ? i_colBear : i_colBull

// End a line at bar x: fade it, or delete it if ended lines are hidden
f_end(TL t, int x) =>
    if i_showEnd
        line.set_xy2(t.ln, x, f_y(t, x))
        line.set_color(t.ln, color.new(f_col(t.side), i_endTr))
    else
        line.delete(t.ln)

// Kill live lines that closed through or reached their life limit
f_checkDeaths(array<TL> live, bool isBear, float tl) =>
    int n = array.size(live)
    if n > 0
        for j = n - 1 to 0
            TL t = array.get(live, j)
            float y = f_y(t, bar_index)
            bool broke = isBear ? close > y + tl : close < y - tl
            if broke or bar_index - t.born > i_maxAge
                f_end(t, bar_index)
                array.remove(live, j)

// Build lines from a newly confirmed swing point
f_build(array<Piv> store, array<TL> live, int side, int pBar, float pPrice, float a, float tl) =>
    bool isH = side == 1
    array<TL> cands = array.new<TL>()
    int n = array.size(store)
    if n > 0 and not na(a)
        for j = n - 1 to math.max(0, n - i_K)
            Piv p1 = array.get(store, j)
            int span = pBar - p1.bar
            bool startOk = isH ? p1.price > pPrice : p1.price < pPrice
            if startOk and span >= 2 and span <= i_maxSpan and bar_index - p1.bar < 900
                float m = (pPrice - p1.price) / span
                if math.abs(m) / a <= i_maxSteep
                    bool ok = true
                    // Rule 1: clean between anchors (wicks)
                    for x = p1.bar + 1 to pBar - 1
                        float yx = p1.price + m * (x - p1.bar)
                        int off = bar_index - x
                        bool thru = isH ? high[off] > yx + tl : low[off] < yx - tl
                        if thru
                            ok := false
                            break
                    // Rule 2: no close through from anchor 2 to confirm bar
                    if ok and bar_index > pBar
                        for x = pBar + 1 to bar_index
                            float yc = p1.price + m * (x - p1.bar)
                            int off2 = bar_index - x
                            bool cthru = isH ? close[off2] > yc + tl : close[off2] < yc - tl
                            if cthru
                                ok := false
                                break
                    if ok
                        array.push(cands, TL.new(side=side, x1=p1.bar, y1=p1.price, m=m, born=bar_index))
    // Rule 5a: steepest (nearest start) + shallowest (farthest start) only
    int nc = array.size(cands)
    if i_stSh and nc > 2
        TL nearT = array.get(cands, 0)
        TL farT  = array.get(cands, nc - 1)
        array.clear(cands)
        array.push(cands, nearT)
        array.push(cands, farT)
    nc := array.size(cands)
    // Add farthest start first, nearest last
    if nc > 0
        for j = nc - 1 to 0
            TL t = array.get(cands, j)
            // Rule 5b: one live line per start point
            if i_oneOrig
                int nl = array.size(live)
                if nl > 0
                    for q = nl - 1 to 0
                        TL o = array.get(live, q)
                        if o.x1 == t.x1
                            f_end(o, bar_index)
                            array.remove(live, q)
            int x2 = bar_index + i_proj
            t.ln := line.new(t.x1, t.y1, x2, f_y(t, x2), xloc=xloc.bar_index, color=f_col(side), width=i_width)
            array.push(live, t)
            if i_showBorn
                label.new(bar_index, f_y(t, bar_index), "", xloc=xloc.bar_index, yloc=yloc.price, style=label.style_circle, color=f_col(side), size=size.tiny)
    // Rule 5c: max live lines per side
    while array.size(live) > i_maxLive
        TL o = array.shift(live)
        f_end(o, bar_index)
    // Store the new swing point
    array.push(store, Piv.new(pBar, pPrice))
    if array.size(store) > 20
        array.shift(store)

// ─── SESSION RESET (optional) ─────────────────────────────────────────────
if session.isfirstbar_regular
    sessBar := bar_index
    if i_today
        for t in bear
            f_end(t, bar_index - 1)
        for t in bull
            f_end(t, bar_index - 1)
        array.clear(bear)
        array.clear(bull)
        array.clear(hiPiv)
        array.clear(loPiv)

// ─── SWING STATE MACHINE (Zen Swing 123 timing, true-extreme anchors) ────
int   pType  = 0
int   pBar   = na
float pPrice = na

if barstate.isfirst
    sHi := high
    sLo := low
else if sDir == 0
    if high > sHi
        sHi  := high
        sDir := 1
    else if low < sLo
        sLo  := low
        sDir := -1
else if sDir == 1
    if high > sHi
        sHi := high
        rc  := 0
    else
        rc := low < low[1] ? rc + 1 : 0
        if rc >= i_thr
            int start = na(lastPiv) ? math.max(0, bar_index - 500) : lastPiv + 1
            int kBest = 0
            float best = high
            for k = 0 to bar_index - start
                if high[k] >= best
                    best  := high[k]
                    kBest := k
            pType   := 1
            pBar    := bar_index - kBest
            pPrice  := best
            lastPiv := pBar
            sLo     := low
            sDir    := -1
            rc      := 0
else
    if low < sLo
        sLo := low
        rc  := 0
    else
        rc := high > high[1] ? rc + 1 : 0
        if rc >= i_thr
            int start = na(lastPiv) ? math.max(0, bar_index - 500) : lastPiv + 1
            int kBest = 0
            float best = low
            for k = 0 to bar_index - start
                if low[k] <= best
                    best  := low[k]
                    kBest := k
            pType   := -1
            pBar    := bar_index - kBest
            pPrice  := best
            lastPiv := pBar
            sHi     := high
            sDir    := 1
            rc      := 0

// ─── SWING POINT MARKERS ──────────────────────────────────────────────────
if pType != 0 and i_showPiv
    label.new(pBar, pPrice, "", xloc=xloc.bar_index, yloc=yloc.price, style=pType == 1 ? label.style_triangledown : label.style_triangleup, color=f_col(pType), size=size.tiny)

// ─── LINE ENDS (before new lines, same as test) ───────────────────────────
f_checkDeaths(bear, true, tol)
f_checkDeaths(bull, false, tol)

// ─── NEW LINES ────────────────────────────────────────────────────────────
if pType != 0 and (not i_today or pBar >= sessBar)
    if pType == 1
        f_build(hiPiv, bear, 1, pBar, pPrice, abr, tol)
    else
        f_build(loPiv, bull, -1, pBar, pPrice, abr, tol)

// ─── LIVE LINES: PROJECT FORWARD + HIDE WHEN FAR (last bar) ──────────────
if barstate.islast
    for t in bear
        int x2 = bar_index + i_proj
        line.set_xy2(t.ln, x2, f_y(t, x2))
        bool tooFar = not na(abr) and math.abs(close - f_y(t, bar_index)) > i_maxDist * abr
        line.set_color(t.ln, color.new(i_colBear, tooFar ? 100 : 0))
    for t in bull
        int x2 = bar_index + i_proj
        line.set_xy2(t.ln, x2, f_y(t, x2))
        bool tooFar = not na(abr) and math.abs(close - f_y(t, bar_index)) > i_maxDist * abr
        line.set_color(t.ln, color.new(i_colBull, tooFar ? 100 : 0))

Overview

Zen Swing Trendlines projects valid trendlines forward while applying rules for maximum slope, bar span, and average bar range tolerance to filter out invalid or broken lines. Faded lines highlight broken or expired levels, while settings let users configure swing confirmation sensitivity, maximum live lines, line lifespan, and visual appearance.

Details

Published by

Zen-Tim on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Trend, Support & resistance

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
Bars against to confirmint1i_thr
Show swing pointsboolfalsei_showPiv
Older swing points to tryint5i_K
Max bars between anchorsint150i_maxSpan
Max steepness (ABR per bar)float1.0i_maxSteep
Tolerance (share of ABR)float0.10i_tolAbr
Steepest + shallowest onlybooltruei_stSh
One live line per start pointbooltruei_oneOrig
Max live lines per sideint3i_maxLive
Line life (bars after it appears)int78i_maxAge
Hide live line beyond (ABR from price)float4.0i_maxDist
Today's session onlyboolfalsei_today
Show all 19 settings
SettingTypeDefaultVariable
Project live lines forward (bars)int10i_proj
Bear linecolor#d1242fi_colBear
Bull linecolor#1f6febi_colBull
Line widthint1i_width
Show broken / expired linesbooltruei_showEnd
Broken / expired transparencyint70i_endTr
Mark bar where line appearsboolfalsei_showBorn
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