Zigzag Trend-based Color [SteinG]
// This source code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// Note: This indicator is forked from Recursive Zigzag [Trendoscope] https://www.tradingview.com/script/J6mxhxdn-Recursive-Zigzag-Trendoscope/
// © SteinG

//@version=5
//Zigzag Trend-based Color [SteinG] v5
// # ========================================================================= #
// #                   |  Zigzag Trend-based Color [SteinG]  |
// # ========================================================================= #
indicator(
 title                = "Zigzag Trend-based Color [SteinG]",
 shorttitle           = "ZG Color",
 overlay              = true,
 max_lines_count      = 500,
 max_labels_count     = 500
 )
// # ========================================================================= #
// #                   |  Zigzag Trend-based Color [SteinG]  |
// # ========================================================================= #

import HeWhoMustNotBeNamed/DrawingTypes/2 as dr
import HeWhoMustNotBeNamed/DrawingMethods/2
import ironpark/TableBuilder/3 as tb


// @function get line style from text input and return as line.style type
// @param lineStyle (string) line style name
// @returns line.style type
getLineStyle(string lineStyle) => 
    switch str.lower(lineStyle)
        "solid" => line.style_solid
        "dashed" => line.style_dashed
        "dotted" => line.style_dotted
        => line.style_solid


// @function Convert float to string format by mintick
// @param _self float
// @returns number string
method tostring_mintick(float _self)=>
    numberFormat = "0."
    zeros = int(math.abs(math.log10(syminfo.mintick)))
    if zeros > 0
        for i = 0 to zeros-1
            numberFormat += "0"
    else
        numberFormat := "0"
    strFormat = '{0,number,'+numberFormat+'}'
    strNumber = str.format(strFormat,_self)


//#region  ************************ Zigzag Types ************************

type PivotCandle
	float _high = high
	float _low = low
	int length = 5
	int pHighBar
	int pLowBar
	float pHigh
	float pLow
    int index = bar_index

type Pivot
	dr.Point point
	int dir
	int level = 0
    int componentIndex = 0
    int subComponents = 0
    float ratio
    float extendRatio

type ZigzagFlags
    bool newPivot = false
    bool doublePivot = false
    bool updateLastPivot = false

type Zigzag
    int length = 5
    int numberOfPivots = 20
    int offset = 0
    int level = 0
    array<Pivot> zigzagPivots
    ZigzagFlags flags

type ZigzagObject
	dr.Line zigzagLine
	dr.Label zigzagLabel

type ZigzagProperties
    bool useSingleLineColor = false
    color lineColor = color.blue
    int lineWidth = 1
    string lineStyle = line.style_solid
    bool showLabel = false
    color textColor = color.black
    int maxObjects = 300
    string xloc = xloc.bar_time

type ZigzagDrawing
    Zigzag zigzag
    ZigzagProperties properties
    array<ZigzagObject> drawings

//#endregion  ************************ 


//#region  ************************ Zigzag Methods ************************

//************************* TYPE PivotCandle  *********************************************//
//calculate highest and lowest price and bars only.
method init(PivotCandle candle)=>
	candle.pHighBar := ta.highestbars(candle._high, candle.length)
	candle.pLowBar := ta.lowestbars(candle._low, candle.length)
	candle.pHigh := ta.highest(candle._high, candle.length)
	candle.pLow := ta.lowest(candle._low, candle.length)
    candle


//************************* TYPE Pivot  ***********************************************//
method unshift(array<Pivot> arr, Pivot val, int maxItems)=>
    arr.unshift(val)
    if(arr.size() > maxItems)
        arr.pop()


//************************* TYPE Zigzag  **********************************************//
//Prepare Pivot Object and ZigzagFlags Ojbect
method init(Zigzag this)=>
    if(na(this.zigzagPivots))
        this.zigzagPivots := array.new<Pivot>()
        this.flags := ZigzagFlags.new()
    this

method addnewpivot(Zigzag this, Pivot pivot)=>
    dir = math.sign(pivot.dir)
    if(this.zigzagPivots.size() >=1)
        lastPivot = this.zigzagPivots.get(0)
        lastValue = lastPivot.point.price
        pivot.subComponents := lastPivot.componentIndex - pivot.componentIndex
        if(math.sign(lastPivot.dir) == math.sign(dir))
            runtime.error('Direction mismatch')
        if(this.zigzagPivots.size() >=2)
            llastPivot = this.zigzagPivots.get(1)
            value = pivot.point.price
            llastValue = llastPivot.point.price
            newDir =  dir * value >= dir * llastValue ? dir * 2 : dir
            pivot.dir := int(newDir)
            pivot.ratio := math.round(math.abs(lastValue - value)/math.abs(llastValue - lastValue), 3)
            if(this.zigzagPivots.size() >=3)
                lllastPivot = this.zigzagPivots.get(2)
                lllastValue = lllastPivot.point.price
                pivot.extendRatio := math.round(math.abs(lastValue - value)/math.abs(lllastValue - llastValue), 3)
    
    this.zigzagPivots.unshift(pivot, this.numberOfPivots)
    this


// @function Calculate zigzag based on input values and indicator values
// @param this Zigzag object
// @param ohlc Array containing OHLC values. Can also have custom values for which zigzag to be calculated
// @returns current Zigzag object
method calculate(Zigzag this, array<float> ohlc) =>
    _ohlc = na(ohlc[this.offset])? ohlc : ohlc[this.offset]
    this.init()
    PivotCandle candle = PivotCandle.new(_ohlc.max(), _ohlc.min(), this.length)
    candle.init() //init includes calculate the highest and lowest price and bars in specific length
    pDir = 1
    Pivot lastPivot = na
    this.flags.updateLastPivot := false
    this.flags.newPivot := false
    this.flags.doublePivot := false
    newBar = bar_index-this.offset
    newbartime = time[this.offset]
    var counter = 0
    var lastCounterBar = 0
    doublePivot = candle.pHighBar == 0 and candle.pLowBar == 0

    if(this.zigzagPivots.size() > 0)
        lastPivot := this.zigzagPivots.get(0)
        pDir := int(math.sign(lastPivot.dir))

    if(this.zigzagPivots.size() > 1)
        llastPivot = this.zigzagPivots.get(1)
        llastDir = int(math.sign(llastPivot.dir))
    
    //current bar is the highest high in uptrend or lowest low in downtrend and already have Pivots data
    //repaint lastest pivot
    if ((pDir == 1 and candle.pHighBar == 0) or (pDir == -1 and candle.pLowBar == 0)) and this.zigzagPivots.size()>=1
        value = pDir == 1 ? candle.pHigh : candle.pLow
        ipivot = pDir == 1? candle.pLow : candle.pHigh
        removeOld = value * lastPivot.dir > lastPivot.point.price * lastPivot.dir
        if(removeOld and not doublePivot)
            this.flags.updateLastPivot := true
            this.flags.newPivot := true
            this.zigzagPivots.shift() //delete last Pivot
            newPivotObject = Pivot.new(dr.Point.new(value, newBar, newbartime), pDir) //create new Pivot as updated Pivot
            this.addnewpivot(newPivotObject)
        
        //current bar is the highest high in uptrend or lowest low in downtrend, 
        //but the price value not greater than the previous pivot in uptrend,
        //or not smaller than the previous pivot in downtrend,
        //add an inverse pivot from the current highest / lowest bar according to zigzag length
        else if(not removeOld and not doublePivot)
            ipivotbar = pDir == 1? newBar+candle.pLowBar : newBar+candle.pHighBar
            ipivottime = time[newBar-ipivotbar+this.offset]
            iCandle = candle[newBar-ipivotbar]
            newPivotObject = Pivot.new(dr.Point.new(ipivot, ipivotbar, ipivottime), -pDir)
            this.addnewpivot(newPivotObject)
            this.flags.newPivot := true

        //current bar is both the highest high and lowest low at the same time
        else if (doublePivot)
            if (pDir == 1 and close > open) or (pDir == -1 and close < open)
                newPivotObject = Pivot.new(dr.Point.new(ipivot, newBar, newbartime), -pDir)
                this.addnewpivot(newPivotObject)
                newPivotObject := Pivot.new(dr.Point.new(value, newBar, newbartime), pDir)
                this.addnewpivot(newPivotObject)
                this.flags.doublePivot := true
                this.flags.newPivot := true

            else if (pDir == 1 and close < open) or (pDir == -1 and close > open)
                if (removeOld)
                    this.flags.updateLastPivot := true
                    this.zigzagPivots.shift() //delete last Pivot
                    newPivotObject = Pivot.new(dr.Point.new(value, newBar, newbartime), pDir)
                    this.addnewpivot(newPivotObject)
                    newPivotObject := Pivot.new(dr.Point.new(ipivot, newBar, newbartime), -pDir)
                    this.addnewpivot(newPivotObject)
                    this.flags.doublePivot := true
                    this.flags.newPivot := true

    //current bar is the lowest low in uptrend or highest high in downtrend
    //directly add pviot point
    //doublePivot excluded
    if (not doublePivot) and ((pDir == 1 and candle.pLowBar == 0) or (pDir == -1 and candle.pHighBar == 0)) and (not this.flags.newPivot)
        value = pDir == 1 ? candle.pLow : candle.pHigh
        newPivotObject = Pivot.new(dr.Point.new(value, newBar, newbartime), -pDir)
        this.addnewpivot(newPivotObject)
        this.flags.newPivot := true

    this


// @function Calculate zigzag based on properties embedded within Zigzag object
// @param this Zigzag object
// @returns current Zigzag object
method calculate(Zigzag this)=>
    this.calculate(array.from(high,low))


//************************* TYPE ZigzagObject  ****************************************//
method push(array<ZigzagObject> arr, val, maxItems)=>
    arr.push(val)
    if(arr.size() > maxItems)
        ZigzagObject lastObject = arr.shift()
        lastObject.zigzagLine.delete()
        lastObject.zigzagLabel.delete()

method pop(array<ZigzagObject> arr)=>
    ZigzagObject lastObject = arr.pop()
    lastObject.zigzagLine.delete()
    lastObject.zigzagLabel.delete()


//************************* TYPE ZigzagDrawing  ***************************************//
method init(ZigzagDrawing this)=>
    if(na(this.drawings))
        this.drawings := array.new<ZigzagObject>()
    if(na(this.zigzag))
        this.zigzag := Zigzag.new()
        this.zigzag.init()
    if(na(this.properties))
        this.properties := ZigzagProperties.new()
    this

method clear(ZigzagDrawing this)=>
    for zigzagObject in this.drawings
        zigzagObject.zigzagLine.delete()
        zigzagObject.zigzagLabel.delete()
    this.drawings.clear()

// @function Clears zigzag drawings array
// @param this array<ZigzagDrawing>
// @returns void
method clear(array<ZigzagDrawing> this)=>
    for drawing in this
        drawing.clear()
    this.clear()

// @function draws fresh zigzag based on properties embedded in ZigzagDrawing object without trying to calculate
// @param this ZigzagDrawing object
// @returns ZigzagDrawing object
method drawplain(ZigzagDrawing this)=>
    this.init()
    this.clear()
    if(barstate.islast or barstate.islastconfirmedhistory)
        for i=0 to this.zigzag.zigzagPivots.size()>1? this.zigzag.zigzagPivots.size()-2 : na
            lastPivot = this.zigzag.zigzagPivots.get(i)
            llastPivot = this.zigzag.zigzagPivots.get(i+1)
            dir = lastPivot.dir
            
            y1 = lastPivot.point.price
            y2 = llastPivot.point.price
            x1 = this.properties.xloc == xloc.bar_index? lastPivot.point.bar : lastPivot.point.bartime
            x2 = this.properties.xloc == xloc.bar_index? llastPivot.point.bar : llastPivot.point.bartime
            //Line
            //Point Start
            startPrice = lastPivot.point.price
            startBar = lastPivot.point.bar
            startBarTime = lastPivot.point.bartime
            dr.Point startPoint = dr.Point.new(startPrice, startBar, startBarTime)
            //Point End
            endPrice = llastPivot.point.price
            endBar = llastPivot.point.bar
            endBarTime = llastPivot.point.bartime
            dr.Point endPoint = dr.Point.new(endPrice, endBar, endBarTime)
            //Line Properties
            lineColor = this.properties.lineColor
            if not this.properties.useSingleLineColor
                lineColor := switch dir
                    1  => color.orange
                    2  => color.green
                    -1 => color.lime
                    -2 => color.red
                    => color.silver
            dr.LineProperties lineprops = dr.LineProperties.new(
                 this.properties.xloc,
                 extend.none,
                 lineColor,
                 this.properties.lineStyle,
                 this.properties.lineWidth
                 )
            dr.Line zline = startPoint.createLine(endPoint, lineprops)
            zline.draw()
            level = lastPivot.level

            //Label
            dr.Label zlabel = dr.Label.new()
            if(this.properties.showLabel)
                labelText = 'Pivot : ' + lastPivot.point.price.tostring_mintick() + '\nRetraced Ratio : '+str.tostring(lastPivot.ratio) + 
                 '\nExtend Ratio : '+str.tostring(lastPivot.extendRatio)
                //Label Properties
                labelColor = switch dir
                    1  => color.orange
                    2  => color.green
                    -1 => color.lime
                    -2 => color.red
                    => color.silver
                dr.LabelProperties labelprops = dr.LabelProperties.new(
                     this.properties.xloc,
                     yloc.price,
                     labelColor,
                     dir > 0? label.style_label_down : label.style_label_up,
                     this.properties.textColor,
                     size.small
                     )
                zlabel := startPoint.createLabel(labelText, properties=labelprops)
                zlabel.draw()
            this.drawings.push(ZigzagObject.new(zline, zlabel))
    this


//#endregion  ************************ 


//#region ************************  main() ************************ 

zigzagLength = input.int(8, step=1, minval=2, title='Length', group='Zigzag', tooltip='Zigzag length')
depth = input.int(500, "Depth", step=25, maxval=500, group='Zigzag', tooltip='Zigzag depth refers to max number of pivots to show on chart')
useOCPrices = input.bool(false, 'Use Open Close Prices', group='Zigzag', 
                         tooltip='Use open close prices for calculating pivots instead of high/low.')

lineWidth = input.int(3, "Line Width", group="Line Style", inline="l")
input_lineStyle = input.string("Solid", "Line Style", options=["Solid", "Dashed", "Dotted"], group="Line Style", inline="l")
useSingleLineColor = input.bool(defval = false, title = "Use Single Color", inline = "Color", group = 'Line Style')
lineColor = input.color(defval = color.blue, title='', inline = 'Color', group = 'Line Style', tooltip='Only applicable when "Use Single Color" is set to "true"')

showLabel = input.bool(true, 'Show Label', group='Label')

ZG_showStats = input.bool(true, 'Show Table', group='Info Table', inline='Info')
ZGtblPosition = input.string(position.top_right, '', group='Info Table', options=[position.top_right, position.bottom_right, position.top_center, position.top_left, position.bottom_left], inline='Info')
ZG_infotbl_textSize = input.string(size.small, '', group='Info Table', options=[size.tiny, size.small, size.normal, size.large, size.huge], inline='Info')

lineStyle = getLineStyle(input_lineStyle)

float bodyTop = math.max(open,close)
float bodyBottom = math.min(open,close)
highSource = useOCPrices? bodyTop : high
lowSource = useOCPrices? bodyBottom : low
ohlc = array.from(highSource, lowSource)

var Zigzag zigzag = Zigzag.new(zigzagLength, depth)
zigzag.calculate(ohlc)
var ZigzagProperties props = ZigzagProperties.new(useSingleLineColor, lineColor, lineWidth, lineStyle, showLabel=showLabel, maxObjects = depth)
var ZigzagDrawing drawing = ZigzagDrawing.new(zigzag, props)
if(barstate.islast)
    drawing.drawplain()

//Zigzag Info Table
ZG_info_title_color = color.maroon
ZG_info_title_txt_color = color.white
ZG_info_no_color = #8BE8FF
ZG_info_id_color = #12B7E9
ZG_info_cell_txt_color = color.black
if(barstate.islast and ZG_showStats)
    var tblstyle = tb.TableStyle.new(bgcolor = color.black,
                                     frame_color = color.black,
                                     frame_width = 1,
                                     border_color = color.black,
                                     border_width = 1
                                     )

    var ZG_infoTbl = tb.new(ZGtblPosition, 8, 21, tblstyle)
    header = ZG_infoTbl.R(0).TextColor(ZG_info_title_txt_color).TextSize(ZG_infotbl_textSize)
    header.Text("No.","BAR ID","DIST","DATETIME","PIVOT VALUE","DIRECTION","RETRACED RATIO","EXTEND RATIO").Bg(ZG_info_title_color)
    
    if array.size(zigzag.zigzagPivots) > 0 and array.size(zigzag.zigzagPivots) <= 20
        for i = 0 to array.size(zigzag.zigzagPivots) - 1
            _str_no          = str.tostring(i+1)
            _str_barID       = str.tostring(zigzag.zigzagPivots.get(i).point.bar)
            _str_dist        = str.tostring(bar_index - zigzag.zigzagPivots.get(i).point.bar)
            _str_time        = str.format_time(zigzag.zigzagPivots.get(i).point.bartime, "Y/M/d HH:mm", syminfo.timezone)
            _str_pivot       = zigzag.zigzagPivots.get(i).point.price.tostring_mintick()
            _str_direction   = switch zigzag.zigzagPivots.get(i).dir
                1  => "↑  (1)"
                2  => "⇈  (2)"
                -1 => "↓  (-1)"
                -2 => "⇊ (-2)"
                => "▣"
            _str_ratio       = str.format("{0,number,0.000}",zigzag.zigzagPivots.get(i).ratio)
            _str_extendRatio = str.format("{0,number,0.000}",zigzag.zigzagPivots.get(i).extendRatio)
            ZG_info_cell_color = switch zigzag.zigzagPivots.get(i).dir
                1  => color.orange
                2  => color.green
                -1 => color.lime
                -2 => color.red
                => color.silver
            ZG_infoTbl.R(i+1).TextColor(ZG_info_cell_txt_color).TextSize(ZG_infotbl_textSize).Text(_str_no, _str_barID, 
                             _str_dist, _str_time, _str_pivot, _str_direction, _str_ratio, _str_extendRatio).Bg(ZG_info_cell_color)
            ZG_infoTbl.R(i+1).C(0).Bg(ZG_info_no_color)
            ZG_infoTbl.R(i+1).C(1).Bg(ZG_info_id_color)
            ZG_infoTbl.R(i+1).C(2).Bg(ZG_info_no_color)
    
    else if array.size(zigzag.zigzagPivots) > 0
        for i = 0 to array.size(zigzag.zigzagPivots) - 1
            if i >= 20
                break
            _str_no          = str.tostring(i+1)
            _str_barID       = str.tostring(zigzag.zigzagPivots.get(i).point.bar)
            _str_dist        = str.tostring(bar_index - zigzag.zigzagPivots.get(i).point.bar)
            _str_time        = str.format_time(zigzag.zigzagPivots.get(i).point.bartime, "Y/M/d HH:mm", syminfo.timezone)
            _str_pivot       = zigzag.zigzagPivots.get(i).point.price.tostring_mintick()
            _str_direction   = switch zigzag.zigzagPivots.get(i).dir
                1  => "↑  (1)"
                2  => "⇈  (2)"
                -1 => "↓  (-1)"
                -2 => "⇊ (-2)"
                => "▣"
            _str_ratio       = str.format("{0,number,0.000}",zigzag.zigzagPivots.get(i).ratio)
            _str_extendRatio = str.format("{0,number,0.000}",zigzag.zigzagPivots.get(i).extendRatio)
            ZG_info_cell_color = switch zigzag.zigzagPivots.get(i).dir
                1  => color.orange
                2  => color.green
                -1 => color.lime
                -2 => color.red
                => color.silver
            ZG_infoTbl.R(i+1).TextColor(ZG_info_cell_txt_color).TextSize(ZG_infotbl_textSize).Text(_str_no, _str_barID, 
                             _str_dist, _str_time, _str_pivot, _str_direction, _str_ratio, _str_extendRatio).Bg(ZG_info_cell_color)
            ZG_infoTbl.R(i+1).C(0).Bg(ZG_info_no_color)
            ZG_infoTbl.R(i+1).C(1).Bg(ZG_info_id_color)
            ZG_infoTbl.R(i+1).C(2).Bg(ZG_info_no_color)

//#endregion ************************ 