lib_zig
// This work 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/
// © robbatt

//@version=5

// @description Object oriented implementation of Pivot methods.
library("lib_zig", overlay=true)
import robbatt/lib_log/6 as LOG
import robbatt/lib_priceaction/6 as PA
import robbatt/lib_plot_objects/34 as D
import robbatt/lib_pivot/47 as P


////////////////////////////////////////////////////
//#region              MODEL
////////////////////////////////////////////////////

export type ZigzagConfig
    D.LineArgs hh_line_args
    D.LineArgs lh_line_args
    D.LineArgs hl_line_args
    D.LineArgs ll_line_args
    D.LabelArgs hh_label_args
    D.LabelArgs lh_label_args
    D.LabelArgs hl_label_args
    D.LabelArgs ll_label_args


export type ZigzagSignals
    bool double_pivot = false
    bool extra_pivot = false
    bool direction_change = false
    bool extra_abstract_pivot = false
    bool last_updated = false


export type Zigzag
    int max_pivots = 30
    P.HLData hldata
    ZigzagConfig config
    int level = 0
    array<P.Pivot> pivots
    ZigzagSignals signals


////////////////////////////////////////////////////
//#endregion              MODEL
////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////
//#region OBJECT INSPECTION / CONVERSION TO JSON read
/////////////////////////////////////////////////////////////
//@function converts object to json representation
export method tostring(ZigzagSignals this) =>
    na(this) ? 'null' : '{' + str.format('"double_pivot": {0}, "extra_pivot": {1}, "direction_change": {2}', this.double_pivot, this.extra_pivot, this.direction_change) + '}'

//@function converts object to json representation
//@param date_format optional date format for str.format'ing point bar_time coordinates (default: 'dd-MM-y HH:mm')
export method tostring(Zigzag this, simple string date_format = 'dd-MM-y HH:mm') =>
    na(this) ? 'null' : '{' + str.format('"max_pivots": {0}, "hldata": {1}, "pivots": {2}, "signals": {3}', this.max_pivots, this.hldata.tostring(), this.pivots.tostring(date_format), this.signals.tostring()) + '}'


/////////////////////////////////////////////////////////////
//#endregion         OBJECT INSPECTION / tostring
/////////////////////////////////////////////////////////////



/////////////////////////////////////////////////////////////
//#region                HELPERS
/////////////////////////////////////////////////////////////

method add_next(Zigzag this, chart.point point, D.Candle candle) =>
    if na(this) or na(point) ? false : not na(this.pivots)
        this.pivots.add_next(point, this.max_pivots, candle)
        this.signals.direction_change := true
    this

method add_next_lowest(Zigzag this) =>
    this.add_next(this.hldata.lowest_point, this.hldata.lowest_candle)

method add_next_highest(Zigzag this) =>
    this.add_next(this.hldata.highest_point, this.hldata.highest_candle)

export method has_new_pivot(ZigzagSignals this) =>
    this.extra_pivot or this.double_pivot or this.direction_change

export method has_new_pivot(Zigzag this) =>
    this.signals.has_new_pivot()

export create_config(D.LineArgs base_line_args, D.LabelArgs base_label_args, P.PivotColors line_colors, P.PivotColors label_text_colors = na, P.PivotColors label_colors = na) =>
    var lbl_colors = na(label_colors) ? P.PivotColors.new(#00000000, #00000000, #00000000, #00000000) : label_colors
    var txt_colors = na(label_text_colors) ? line_colors : label_text_colors
    ZigzagConfig.new(
     base_line_args.copy().set_line_color(line_colors.get_color(2)),
     base_line_args.copy().set_line_color(line_colors.get_color(1)),
     base_line_args.copy().set_line_color(line_colors.get_color(-1)),
     base_line_args.copy().set_line_color(line_colors.get_color(-2)),
     base_label_args.copy().set_bg_color(lbl_colors.get_color(2)).set_txt_color(txt_colors.get_color(2)).set_style(label.style_label_down),
     base_label_args.copy().set_bg_color(lbl_colors.get_color(1)).set_txt_color(txt_colors.get_color(1)).set_style(label.style_label_down),
     base_label_args.copy().set_bg_color(lbl_colors.get_color(-1)).set_txt_color(txt_colors.get_color(-1)).set_style(label.style_label_up),
     base_label_args.copy().set_bg_color(lbl_colors.get_color(-2)).set_txt_color(txt_colors.get_color(-2)).set_style(label.style_label_up)
     )

export create_config(D.LineArgs base_line_args, D.LabelArgs base_label_args, color simple_color) =>
    create_config(base_line_args, base_label_args, P.PivotColors.new(simple_color, simple_color, simple_color, simple_color))

export method set_config(Zigzag this, ZigzagConfig config) =>
    this.config := config
    this

export method get_line_args(Zigzag this, int mode) =>
    switch mode
        2 => this.config.hh_line_args
        1 => this.config.lh_line_args
        -1 => this.config.hl_line_args
        -2 => this.config.ll_line_args
        => na

export method get_label_args(Zigzag this, int mode) =>
    switch mode
        2 => this.config.hh_label_args
        1 => this.config.lh_label_args
        -1 => this.config.hl_label_args
        -2 => this.config.ll_label_args
        => na


/////////////////////////////////////////////////////////////
//#endregion                HELPERS
/////////////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////
//#region         OBJECT UPDATE / update
/////////////////////////////////////////////////////////////
export method init(Zigzag this) =>
    if not na(this)
        if na(this.pivots)
            this.pivots := array.new<P.Pivot>()
        if na(this.hldata)
            this.hldata := P.HLData.new()
        if na(this.signals)
            this.signals := ZigzagSignals.new()
        if na(this.config)
            this.config := ZigzagConfig.new(
             D.LineArgs.new(),
             D.LineArgs.new(),
             D.LineArgs.new(),
             D.LineArgs.new(),
             D.LabelArgs.new(),
             D.LabelArgs.new(),
             D.LabelArgs.new().set_style(label.style_label_up),
             D.LabelArgs.new().set_style(label.style_label_up)
             )

        // reset signals
        this.signals.double_pivot := false
        this.signals.extra_pivot := false
        this.signals.direction_change := false
        this.signals.extra_abstract_pivot := false
        this.signals.last_updated := false

    this

export method initPivots(Zigzag this) =>
	if this.pivots.size() == 0 and not na(this.hldata.lowest) and not na(this.hldata.highest)
		if this.hldata.lowest_offset > this.hldata.highest_offset
			this.add_next_lowest()
			this.add_next_highest()
		else
			this.add_next_highest()
			this.add_next_lowest()
		this.signals.extra_pivot := true
		this.signals.direction_change := true

//@function update the current Zigzag, call this on every iteration, for parameters of length/period and input source series, pass a HLData instance
//@param ref_highest The seriest to detect high pivots from
//@param ref_lowest The seriest to detect low pivots from
//@param add_intermediate_fractals_on_force when forcing a trend change during an ongoing sideways trend, this adds intermediate fractals as additional pivots between the last pivot and the forced pivot ( default: true)
//@param ignore_double_pivots_in_trend_direction ignores a double pivot when it's candle aligns with the trend direction (e.g. double pivot in bullish trend, but the candle closes bullish, means inside the candle there was a significant move down, but it was already mitigated and the candle closed bullish with a hh, so chances are higher that trend is continued)
export method update(Zigzag this, float ref_highest = high, float ref_lowest = low, bool add_intermediate_fractals_on_force = true, bool ignore_double_pivots_in_trend_direction = true) =>
    this.init()
    pd = this.hldata.update(ref_highest, ref_lowest)
	this.initPivots()
    count = this.pivots.size()

    if count >= 2
        P.Pivot sec_last = this.pivots.get(1)
        P.Pivot last = this.pivots.get(0)
        int trend = last.direction()
        int bars_since_last = bar_index - last.point.index
        bool stalling = bars_since_last >= pd.length and barstate.isconfirmed
        bool double_pivot = pd.new_highest and pd.new_lowest and (not ignore_double_pivots_in_trend_direction or pd.current_candle().direction() != trend and barstate.isconfirmed)
        
        if trend == 1
            // force low (sideways, lower highs, no new lowest for trend change)

            forced_l = stalling and not pd.new_lowest and not last.exceeded_by(pd.highest) and (add_intermediate_fractals_on_force or pd.new_highest)
            if forced_l
                this.add_next_lowest()

            // trend continuation
            if pd.new_highest
                if forced_l and add_intermediate_fractals_on_force
                    this.add_next_highest()
                    this.signals.extra_pivot := true
                // trend continuation
                else if last.exceeded_by(pd.highest)
                    this.pivots.update_last(pd.highest_point, pd.highest_candle)
                    this.signals.last_updated := true

            // double pivot
            if double_pivot
                this.add_next_lowest()
                this.signals.double_pivot := true
            // trend change
            else if pd.new_lowest and pd.highest_candle.l > pd.lowest
                this.add_next_lowest()

        if trend == -1
            // force high (sideways, higher lows, no new highest for trend change)
            forced_h = stalling and not pd.new_highest and not last.exceeded_by(pd.lowest) and (add_intermediate_fractals_on_force or pd.new_lowest)
            if forced_h
                this.add_next_highest()

            if pd.new_lowest
                // trend change after forced h
                if forced_h and add_intermediate_fractals_on_force
                    this.add_next_lowest()
                    this.signals.extra_pivot := true
                // trend continuation
                else if last.exceeded_by(pd.lowest)
                    this.pivots.update_last(pd.lowest_point, pd.lowest_candle)
                    this.signals.last_updated := true

            // double pivot
            if double_pivot
                this.add_next_highest()
                this.signals.double_pivot := true
            // trend change 
            else if pd.new_highest and pd.lowest_candle.h < pd.highest
                this.add_next_highest()
    
    this



// @function remove intermediate HL and LH
// @param this Zigzag object
// @returns Higher level abstraction Zigzag object
export method get_abstraction(Zigzag this, ZigzagConfig config = na) =>
    // TODO speed optimize if not live, only update when [1] changed, if live when [0] change
    abstraction = Zigzag.new(this.max_pivots, this.hldata, na(config) ? this.config : config, this.level + 1)
    abstraction.init()
    count = this.pivots.size()

    if count >= 1
        P.Pivot possible_trend_change_pivot_bullish = na
        P.Pivot possible_trend_change_pivot_bearish = na
        
        for i = count-1 to 0
            P.Pivot pivot = P.Pivot.copy(this.pivots.get(i))

            if abstraction.pivots.size() > 0 // if there are already other higher level pivot points

                P.Pivot last = abstraction.pivots.get(0)
                pivot_dir = pivot.direction()
                trend = last.direction()
                is_hhll = math.abs(pivot.mode) == 2

                if is_hhll // (only interested in HH/LLs)
                
                    if trend == pivot_dir // and the current pivot is the same direction as the previous one

                        if last.exceeded_by(pivot.point.price) // ignore lower level direction
                            abstraction.pivots.update_last(pivot.point)
                        else
                            // or confirmation of a set-aside trend change
                            temp_pivot = pivot_dir == 1 ? possible_trend_change_pivot_bearish : possible_trend_change_pivot_bullish
                            if not na(temp_pivot)
                                abstraction.add_next(temp_pivot.point, temp_pivot.candle)
                                abstraction.add_next(pivot.point, pivot.candle)
                            else
                                // if there's none set aside, drop this pivot
                                continue 

                    else
                        // if not the same direction as the previous pivot, this COULD be a trend change
                        // collect the last pivots that were set aside
                        P.Pivot temp_pivot_same_direction  = pivot_dir == 1 ? possible_trend_change_pivot_bullish : possible_trend_change_pivot_bearish
                        P.Pivot temp_pivot_other_direction = pivot_dir == 1 ? possible_trend_change_pivot_bearish : possible_trend_change_pivot_bullish

                        if not temp_pivot_same_direction.exceeded_by(pivot.point.price)
                            abstraction.add_next(temp_pivot_same_direction.point, temp_pivot_same_direction.candle)
                            abstraction.add_next(temp_pivot_other_direction.point, temp_pivot_other_direction.candle)
                            abstraction.add_next(pivot.point, pivot.candle)
                            abstraction.signals.extra_pivot := true
                            abstraction.signals.extra_abstract_pivot := true
                        else
                            abstraction.add_next(pivot.point, pivot.candle)
                        
                    // store the current one, reset the set aside ones
                    possible_trend_change_pivot_bullish := na
                    possible_trend_change_pivot_bearish := na
                    
                // if the current pivot is not a HH/LL but something in between, set it aside for now, see what the next ones bring    
                else
                    temp_pivot = pivot_dir == 1 ? possible_trend_change_pivot_bullish : possible_trend_change_pivot_bearish
                    // if there is already one set aside, check if the current one exceeds that then replace the one set aside
                    if na(temp_pivot) ? true : temp_pivot.exceeded_by(pivot)
                        if pivot_dir == 1
                            possible_trend_change_pivot_bullish := pivot
                        else
                            possible_trend_change_pivot_bearish := pivot
                    // else there's already a temp and pivot doesn't exceed it, ignore pivot
                    

            // else if there are no others yet, and the current one is a HH/LL, then store that
            else if math.abs(pivot.mode) == 2
                if not na(pivot.prev)
                    abstraction.pivots.unshift(pivot.prev)
                    abstraction.add_next(pivot.point, pivot.candle)
                else
                    abstraction.pivots.unshift(pivot)
                    abstraction.add_next(pivot.point, pivot.candle)
            // else just skip this one

    abstraction

/////////////////////////////////////////////////////////////
//#endregion         OBJECT UPDATE / update
/////////////////////////////////////////////////////////////



/////////////////////////////////////////////////////////////
//#region         OBJECT CONVERSION / draw
/////////////////////////////////////////////////////////////

//@function draw the Zigzag on the chart, continously
export method draw_continous(Zigzag this, bool live = false) =>
    var D.Line current_line = D.Line.new()
    var D.Label current_label = D.Label.new()
    var current_line_style_backup = line.style_solid

    if this.pivots.size() >= 3
        P.Pivot last = this.pivots.get(0)
        P.Pivot sec_last = this.pivots.get(1)
        P.Pivot thr_last = this.pivots.get(2)

        if live
            // finish previous line and label before switching to double/extra pivot
            if this.signals.double_pivot or this.signals.extra_pivot
                pivot = this.signals.extra_pivot ? thr_last : sec_last
                current_label.txt := pivot.get_label_text()
                current_label.apply_style(this.get_label_args(pivot.mode)).draw()
                current_line.apply_style(this.get_line_args(pivot.mode)).draw(extend_only = false) // continuation of first double pivot as well as pivot before intermediate are confirmed instantly
            
            // on dir change create a copy of the old line for the next
            if this.signals.direction_change
                current_line.plot.copy().set_style(current_line_style_backup) // previous pivot is confirmed
                current_label.plot.copy()
            
            // draw intermediate
            if this.signals.extra_pivot
                current_line.start := thr_last.point
                current_line.end := sec_last.point
                current_line.apply_style(this.get_line_args(sec_last.mode)).draw(extend_only = false) // itermediate is confirmed instantly
                current_line.plot.copy()    
                
                current_label.point := sec_last.point
                current_label.txt := sec_last.get_label_text()
                current_label.apply_style(this.get_label_args(sec_last.mode)).draw()
                current_label.plot.copy()

            // draw last pivot in array
            current_line.start := sec_last.point
            current_line.end := last.point
            current_label.point := last.point
            current_label.txt := last.get_label_text()
            current_line_style_backup := this.get_line_args(last.mode).style
            current_line.apply_style(this.get_line_args(last.mode)).draw(extend_only = false).plot.set_style(line.style_dashed) // new pivot is temporary, make line dashed
            current_label.apply_style(this.get_label_args(last.mode)).draw()

        else if this.has_new_pivot()
            thr_last.point.create_line(sec_last.point).draw(extend_only = false).apply_style(this.get_line_args(sec_last.mode))
            sec_last.point.create_label(sec_last.get_label_text()).draw().apply_style(this.get_label_args(sec_last.mode))
    this

//@function draw the Zigzag on the chart, all at once
export method draw(Zigzag this, bool live = false)=>
    var line[] lines = array.new<line>()
    var label[] labels = array.new<label>()

    count = na(this) ? 0 : this.pivots.size()

    if barstate.islast and count >= 3
        lines.delete().clear()
		labels.delete().clear()

		// draw from present to past
        for i = (live?0:1) to count - 2
    		curr = this.pivots.get(i)
            prev = this.pivots.get(i+1)
            line_args = this.get_line_args(curr.mode)
            label_args = this.get_label_args(curr.mode)
            lines.push(line.new(prev.point, curr.point, this.hldata.xloc, line_args.extend, line_args.line_color, i == 0 ? line.style_dashed : line_args.style, line_args.width))
            labels.push(label.new(curr.point, curr.get_label_text(), this.hldata.xloc, label_args.yloc, label_args.bg_color, label_args.style, label_args.text_color, label_args.size, label_args.text_align, label_args.text_font_family))                
			prev := curr
    this

/////////////////////////////////////////////////////////////
//#endregion         OBJECT CONVERSION / draw
/////////////////////////////////////////////////////////////




/////////////////////////////////////////////////////////////
//#region                   DEMO
/////////////////////////////////////////////////////////////
var lenth = input.int(5, 'length')
var depth = input.int(50, 'depth')
var highlight_level = input.int(0, 'highlight level', 0, 2)
var P.PivotColors l0_colors = P.PivotColors.new()
var P.PivotColors l1_colors = P.PivotColors.new(color.blue, color.fuchsia, color.aqua, color.purple)
var D.LineArgs highlight_line_args = D.LineArgs.new(chart.fg_color, line.style_solid, 2)
var D.LineArgs background_line_args = D.LineArgs.new(chart.fg_color, line.style_dotted, 1)
var D.LabelArgs highlight_label_args = D.LabelArgs.new(chart.fg_color, chart.bg_color, size = size.normal)
var D.LabelArgs background_label_args = D.LabelArgs.new(chart.fg_color, chart.bg_color)
var ZigzagConfig zz0_config = create_config(highlight_level == 0 ? highlight_line_args : background_line_args, highlight_level == 0 ? highlight_label_args : background_label_args, l0_colors)
var ZigzagConfig zz1_config = create_config(highlight_level == 1 ? highlight_line_args : background_line_args, highlight_level == 1 ? highlight_label_args : background_label_args, l1_colors)
var ZigzagConfig zz2_config = create_config(highlight_level == 2 ? highlight_line_args : background_line_args, highlight_level == 2 ? highlight_label_args : background_label_args, color.black)


var Zigzag zz = Zigzag.new(depth, P.HLData.new(lenth, xloc.bar_time), zz0_config)
live = input.bool(false, 'live plot')
zz.update()
Zigzag zz1 = zz.get_abstraction(zz1_config)
Zigzag zz2 = zz1.get_abstraction(zz2_config)

if input.bool(true, 'level 0')
    zz.draw_continous(live)

if barstate.islast
    if input.bool(true, 'level 1')
        zz1.draw(live)
    if input.bool(true, 'level 2')
        zz2.draw(live)

/////////////////////////////////////////////////////////////
//#endregion                   DEMO
/////////////////////////////////////////////////////////////