// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © TradingView
//@version=5
library("ZigZag", overlay = true)
// ZigZag Library
// v7, 2023.10.26
// This code was written using the recommendations from the Pine Script™ User Manual's Style Guide:
// https://www.tradingview.com/pine-script-docs/en/v5/writing/Style_guide.html
//#region ———————————————————— Library types and functions
// @type Provides calculation and display properties to `ZigZag` objects.
// @field devThreshold The minimum percentage deviation from a point before the `ZigZag` changes direction.
// @field depth The number of bars required for pivot detection.
// @field lineColor The color of each line drawn by the `ZigZag`.
// @field extendLast A condition allowing a line to connect the most recent pivot with the current close.
// @field displayReversalPrice A condition to display the pivot price in the pivot label.
// @field displayCumulativeVolume A condition to display the cumulative volume for the pivot segment in the pivot label.
// @field displayReversalPriceChange A condition to display the change in price or percent from the previous pivot in each pivot label.
// @field differencePriceMode The reversal change display mode. Options are "Absolute" or "Percent".
// @field draw A condition to determine whether the `ZigZag` displays lines and labels.
// @field allowZigZagOnOneBar A condition to allow double pivots i.e., when a large bar makes both a pivot high and a pivot low.
export type Settings
float devThreshold = 5.0
int depth = 10
color lineColor = #2962FF
bool extendLast = true
bool displayReversalPrice = true
bool displayCumulativeVolume = true
bool displayReversalPriceChange = true
string differencePriceMode = "Absolute"
bool draw = true
bool allowZigZagOnOneBar = true
// @type Represents a significant level that indicates directional movement or potential support and resistance.
// @field ln A `line` object connecting the `start` and `end` chart points.
// @field lb A `label` object to display pivot values.
// @field isHigh A condition to determine whether the pivot is a pivot high.
// @field vol The cumulative volume for the pivot segment.
// @field start A `chart.point` object representing the coordinates of the previous point.
// @field end A `chart.point` object representing the coordinate of the current point.
export type Pivot
line ln
label lb
bool isHigh
float vol
chart.point start
chart.point end
// @type An object to maintain a Zig Zag's settings, pivots, and cumulative volume.
// @field settings A `Settings` object to provide calculation and display properties.
// @field pivots An array of `Pivot` objects.
// @field sumVol The volume sum for the current `Pivot` object's line segment.
// @field extend A `Pivot` object used to project a line from the last pivot point to the current bar.
export type ZigZag
Settings settings
array<Pivot> pivots
float sumVol = 0
Pivot extend = na
// @function Identifies a pivot point when the `src` has not reached beyond its value
// from `length` bars ago. Finds pivot highs when `isHigh` is `true`, and
// finds pivot lows otherwise.
// @param src (series float) The data series to calculate the pivot value from.
// @param length (series float) The length in bars required for pivot confirmation.
// @param isHigh (simple bool) Determines whether the pivot is a pivot high or pivot low.
// @returns (chart.point) A `chart.point` object when a pivot is found, `na` otherwise.
findPivotPoint(series float src, series float length, simple bool isHigh) =>
float pivotPrice = nz(src[length])
if length == 0
chart.point.new(time, bar_index, pivotPrice)
else if length * 2 <= bar_index
bool isFound = true
for i = 0 to math.abs(length - 1)
if (isHigh and src[i] > pivotPrice) or (not isHigh and src[i] < pivotPrice)
isFound := false
break
for i = length + 1 to 2 * length
if (isHigh and src[i] >= pivotPrice) or (not isHigh and src[i] <= pivotPrice)
isFound := false
break
if isFound
chart.point.new(time[length], bar_index[length], pivotPrice)
// @function Calculates the deviation percentage between the `price` and the `basePrice`.
// @param basePrice (series float) The start price.
// @param price (series float) The end price.
// @returns (float) The signed deviation percentage.
calcDev(series float basePrice, series float price) =>
float result = 100 * (price - basePrice) / math.abs(basePrice)
// @function Calculates the difference between the `start` and `end` point as a price or
// percentage difference and converts its value to a "string".
// @param start (series float) The start price.
// @param end (series float) The end price.
// @param settings (series Settings) A `Settings` object.
// @returns (string) A "string" representation of the difference between points.
priceRotationDiff(series float start, series float end, Settings settings) =>
float diff = end - start
string sign = math.sign(diff) > 0 ? "+" : ""
string diffStr = switch settings.differencePriceMode
"Absolute" => str.tostring(diff, format.mintick)
=> str.tostring(diff * 100 / start, format.percent)
string result = str.format("({0}{1})", sign, diffStr)
// @function Creates a "string" containing the price, cumulative volume, and change in price
// for the pivot.
// @param start (series float) The start price.
// @param end (series float) The end price.
// @param vol (series float) The pivot's cumulative volume.
// @param settings (series Settings) A `Settings` object.
// @returns (string) A "string" to display in pivot labels.
priceRotationAggregate(series float start, series float end, series float vol, Settings settings) =>
string str = ""
if settings.displayReversalPrice
str += str.tostring(end, format.mintick) + " "
if settings.displayReversalPriceChange
str += priceRotationDiff(start, end, settings) + " "
if settings.displayCumulativeVolume
str += "\n" + str.tostring(vol, format.volume)
str
// @function Creates a label with coordinates from the `point` if the `settings` display
// properties allow it.
// @param isHigh (series bool) The condition to determine the label's color and location.
// @param point (series chart.point) A `chart.point` object.
// @param settings (series Settings) A `Settings` object.
// @returns (void) The function does not return a value.
makePivotLabel(series bool isHigh, chart.point point, Settings settings) =>
if settings.displayReversalPrice or settings.displayReversalPriceChange or settings.displayCumulativeVolume
[yloc, txtColor] = switch
isHigh => [yloc.abovebar, color.green]
=> [yloc.belowbar, color.red]
label.new(point, style = label.style_none, xloc = xloc.bar_time, yloc = yloc, textcolor = txtColor)
// @function Updates a `Pivot` object's properties, including its `end` point,
// cumulative volume, label text, and label and line drawing locations.
// Can be used as a function or method.
// @param this (series Pivot) The `Pivot` object to update.
// @param end (series chart.point) A new `chart.point` for the `end` field of the `Pivot`.
// @param vol (series float) The cumulative volume of the `Pivot`.
// @param settings (series Settings) A `Settings` object.
// @returns (void) The function does not return a value.
method updatePivot(Pivot this, chart.point end, float vol, Settings settings) =>
this.end := end
this.vol := vol
if not na(this.lb)
this.lb.set_point(this.end)
this.lb.set_text(priceRotationAggregate(this.start.price, this.end.price, this.vol, settings))
this.ln.set_second_point(this.end)
// @function Creates a new `Pivot` object, and assigns a line and label if the `draw` field
// of the `settings` allows it.
// @param start (series chart.point) A `chart.point` for the `start` of the `Pivot`.
// @param end (series chart.point) A `chart.point` for the `end` of the `Pivot`.
// @param vol (series float) The cumulative volume of the `Pivot`.
// @param isHigh (series bool) Specifies whether the `Pivot` represents a pivot high or pivot low.
// @param settings (series settings) A `Settings` object.
// @returns (Pivot) The new `Pivot` object.
newPivot(
series chart.point start, series chart.point end, series float vol, series bool isHigh, series Settings settings
) =>
Pivot p = Pivot.new(na, na, isHigh, vol, start, end)
if settings.draw
p.ln := line.new(start, end, xloc = xloc.bar_time, color = settings.lineColor, width = 2)
p.lb := makePivotLabel(isHigh, end, settings)
p.updatePivot(end, vol, settings)
p
// @function Deletes the `line` and `label` objects assigned to the `ln` and `lb` fields in
// a `Pivot` object.
// Can be used as a function or method.
// @param this (series Pivot) The `Pivot` object to modify.
// @returns (void) The function does not return a value.
method delete(series Pivot this) =>
if not na(this.ln)
this.ln.delete()
if not na(this.lb)
this.lb.delete()
// @function Determines whether the `price` of the `point` reaches past the `price` of the
// `end` chart point of a `Pivot` object.
// Can be used as a function or method.
// @param this (series Pivot) A `Pivot` object.
// @param point (series chart.point) A `chart.point` object.
// @returns (bool) `true` if the `price` of the `point` reaches past that of the `end`
// in the `Pivot` object, `false` otherwise.
method isMorePrice(series Pivot this, series chart.point point) =>
int m = this.isHigh ? 1 : -1
bool result = point.price * m > this.end.price * m
// @function Returns the last `Pivot` object from a `ZigZag` instance if it contains at
// least one `Pivot`, and `na` otherwise.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @returns (Pivot) The last `Pivot` object in the `ZigZag`.
export method lastPivot(series ZigZag this) =>
int numberOfPivots = this.pivots.size()
Pivot result = numberOfPivots > 0 ? this.pivots.get(numberOfPivots - 1) : na
// @function Updates the fields of the last `Pivot` in a `ZigZag` object and sets the
// `sumVol` of the `ZigZag` to 0.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @param point (series chart.point) The `chart.point` for the `start` of the last `Pivot`.
// @returns (void) The function does not return a value.
method updateLastPivot(series ZigZag this, series chart.point point) =>
Pivot lastPivot = this.lastPivot()
if this.pivots.size() == 1
lastPivot.start := point
if this.settings.draw
lastPivot.ln.set_first_point(point)
lastPivot.updatePivot(point, lastPivot.vol + this.sumVol, this.settings)
this.sumVol := 0
// @function Pushes a new `Pivot` object into the `pivots` array of a `ZigZag` instance.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @param new (series Pivot) The new `Pivot` to add to the ZigZag.
// @returns (void) The function does not return a value.
method newPivotFound(series ZigZag this, series Pivot new) =>
this.pivots.push(new)
this.sumVol := 0
// @function Determines if a new pivot point is detected or if the properties of the
// last `Pivot` in the `ZigZag` need to be updated by comparing the `end` of the
// last `Pivot` to a new `point`. Updates the `ZigZag` and returns `true` if
// either condition occurs.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @param isHigh (series bool) Determines whether it checks for a pivot high or pivot low.
// @param point (chart.point) A `chart.point` to compare to the `end` of the last
// `Pivot` in the `ZigZag`.
// @returns (bool) `true` if it updates the last `Pivot` or adds a new `Pivot` to
// the `ZigZag`, `false` otherwise.
method newPivotPointFound(series ZigZag this, simple bool isHigh, series chart.point point) =>
bool result = false
Pivot lastPivot = this.lastPivot()
if not na(lastPivot)
if lastPivot.isHigh == isHigh
if lastPivot.isMorePrice(point)
this.updateLastPivot(point)
result := true
else
float dev = calcDev(lastPivot.end.price, point.price)
if (not lastPivot.isHigh and dev >= this.settings.devThreshold) or
(lastPivot.isHigh and dev <= -1 * this.settings.devThreshold)
newPivotFound(this, newPivot(lastPivot.end, point, this.sumVol, isHigh, this.settings))
result := true
else
this.newPivotFound(newPivot(point, point, this.sumVol, isHigh, this.settings))
result := true
result
// @function Tries to find a new pivot point for the `ZigZag` instance. Updates the
// `ZigZag` and returns `true` when it registers a detected pivot.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @param src (series float) The data series to calculate the pivot value from.
// @param isHigh (simple bool) Determines whether it checks for a pivot high or pivot low.
// @param depth (series int) The number of bars to search for new pivots.
// @param registerPivot (series bool) A condition that determines whether or not to register a pivot.
// @returns (bool) `true` when a new pivot point is registered and the `ZigZag` is updated,
// `false` otherwise.
method tryFindPivot(
series ZigZag this, series float src, simple bool isHigh, series int depth, series bool registerPivot = true
) =>
chart.point point = findPivotPoint(src, depth, isHigh)
bool result = not na(point) and registerPivot ? this.newPivotPointFound(isHigh, point) : false
// @function Updates a `ZigZag` objects with new pivots, volume, lines, and labels.
// NOTE: This function must be called on every bar for accurate calculations.
// Can be used as a function or method.
// @param this (series ZigZag) A `ZigZag` object.
// @returns (bool) `true` when a new pivot point is registered and the `ZigZag` is updated,
// `false` otherwise.
export method update(series ZigZag this) =>
int depth = math.max(2, math.floor(this.settings.depth / 2))
this.sumVol += nz(volume[depth])
bool somethingChanged = this.tryFindPivot(high, true, depth)
somethingChanged := this.tryFindPivot(
low, false, depth, this.settings.allowZigZagOnOneBar or not somethingChanged
) or somethingChanged
Pivot lastPivot = this.lastPivot()
float remVol = math.sum(volume, math.max(depth, 1))
if this.settings.extendLast and barstate.islast and not na(lastPivot)
bool isHigh = not lastPivot.isHigh
float curSeries = isHigh ? high : low
chart.point end = chart.point.new(time, bar_index, curSeries)
if na(this.extend) or somethingChanged
if not na(this.extend)
this.extend.delete()
this.extend := newPivot(lastPivot.end, end, this.sumVol, isHigh, this.settings)
this.extend.updatePivot(end, this.sumVol + remVol, this.settings)
somethingChanged
// @function Instantiates a new `ZigZag` object with optional `settings`.
// If no `settings` are provided, creates a `ZigZag` object with default settings.
// @param settings (series Settings) A `Settings` object.
// @returns (ZigZag) A new `ZigZag` instance.
export newInstance(series Settings settings = na) =>
ZigZag result = ZigZag.new(na(settings) ? Settings.new() : settings, array.new<Pivot>())
//#endregion
//#region ———————————————————— Example Code
// @variable The deviation percentage from the last local high or low required to form a new Zig Zag point.
float deviationInput = input.float(5.0, "Deviation (%)", minval = 0.00001, maxval = 100.0)
// @variable The number of bars in the pivot calculation.
int depthInput = input.int(10, "Depth", minval = 1)
// @variable The color of the Zig Zag's lines.
color lineColorInput = input.color(#2962FF, "Line Color")
// @variable If `true`, the Zig Zag will also display a line connecting the last known pivot to the current `close`.
bool extendInput = input.bool(true, "Extend to Last Bar")
// @variable If `true`, the pivot labels will display their price values.
bool showPriceInput = input.bool(true, "Display Reversal Price")
// @variable If `true`, each pivot label will display the volume accumulated since the previous pivot.
bool showVolInput = input.bool(true, "Display Cumulative Volume")
// @variable If `true`, each pivot label will display the change in price from the previous pivot.
bool showChgInput = input.bool(true, "Display Reversal Price Change", inline = "Price Rev")
// @variable Controls whether the labels show price changes as raw values or percentages when `showChgInput` is `true`.
string priceDiffInput = input.string("Absolute", "", options = ["Absolute", "Percent"], inline = "Price Rev")
// @variable A `Settings` instance for `ZigZag` creation.
var Settings settings =
Settings.new(
deviationInput, depthInput,
lineColorInput, extendInput,
showPriceInput, showVolInput,
showChgInput, priceDiffInput
)
// @variable A `ZigZag` object created using the `settings`.
var ZigZag zigZag = newInstance(settings)
// Update the `zigZag` on every bar.
zigZag.update()
//#endregion
Show all 365 lines