// 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