lib_pivot
// 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_pivot", overlay=true)
import robbatt/lib_plot_objects/34 as D
import robbatt/lib_priceaction/6 as PA

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

// @type HLData wraps the data received from ta.highest, ta.highestbars, ta.lowest, ta.lowestbars, as well as the reference sources
// @field length lookback length for pivot points
// @field highest_offset offset to highest value bar
// @field lowest_offset offset to lowest value bar
// @field highest highest value within lookback bars
// @field lowest lowest value within lookback bars
// @field new_highest update() will set this true if the current candle forms a new highest high at the last (current) bar of set period (length)
// @field new_lowest update() will set this true if the current candle forms a new lowest low at the last (current) bar of set period (length)
// @field new_highest_fractal update() will set this true if the current candle forms a new fractal high at the center of set period (length)
// @field new_lowest_fractal update() will set this true if the current candle forms a new fractal low at the center of set period (length)
// @field highest_candle ohlc + index data at the bar with the highest value within lookback bars
// @field lowest_candle ohlc + index data at the bar with the lowest value within lookback bars
// @field highest_point data of the highest point within lookback bars
// @field lowest_point data of the lowest point within lookback bars
// @field candle_buffer buffers candle data to prevent max_bars_back compiler error (internal use)
export type HLData
	int length = 5
    string xloc = xloc.bar_index
	int highest_offset
	int lowest_offset
	float highest
	float lowest
    bool new_highest = false
    bool new_lowest = false
    bool new_highest_fractal = false
    bool new_lowest_fractal = false
    D.Candle highest_candle = na
    D.Candle lowest_candle = na
    chart.point highest_point = na
    chart.point lowest_point = na
    D.Candle[] candle_buffer = na

// @type Pivot colors for different modes
// @field hh Color for Pivot mode 2 (HH)
// @field lh Color for Pivot mode 1 (LH)
// @field hl Color for Pivot mode -1 (HL)
// @field ll Color for Pivot mode -2 (LL)
export type PivotColors
    color hh = color.green  // mode 2
    color lh = color.orange   // mode 1
    color hl = color.lime // mode -1
    color ll = color.red    // mode -2


// @type Pivot additional pivot data around basic Point
// @field point
// @field mode can be -2/-1/1/2 for LL/HL/LH/HH
// @field price_movement The price difference between this and the previous pivot point in the opposite direction
// @field retracement_ratio The ratio between this price_movement and the previous
export type Pivot
	chart.point point
    int mode = 1
    float price_movement = 0
    float retracement_ratio = 0
    Pivot prev = na
    D.Candle candle = na

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


/////////////////////////////////////////////////////////////
//#region OBJECT INSPECTION / CONVERSION TO JSON read
/////////////////////////////////////////////////////////////

// @function Converts HLData to a json string representation
// @param this HLData
// @param date_format the format for Point bar_time (default: "y-MM-dd HH:mm")
// @returns string representation of Pivot
export method tostring(HLData this, simple string date_format = "yyyy-MM-dd HH:mm")=>
	na(this)?'null':'{' + str.format('""length": {0}, "xloc": "{1}"", "highest_offset": {2}, "lowest_offset": {3}, "highest": {4}, "lowest": {5}, "new_highest": {6}, "new_lowest": {7}, "new_highest_fractal": {8}, "new_lowest_fractal": {9}, "highest_point": {10}, "lowest_point": {11}, "highest_candle": {12}, "lowest_candle": {13}', this.length, this.xloc, this.highest_offset, this.lowest_offset, this.highest, this.lowest, this.new_highest, this.new_lowest, this.new_highest_fractal, this.new_lowest_fractal, this.highest_point.tostring(date_format), this.lowest_point.tostring(date_format), this.highest_candle.tostring(date_format), this.lowest_candle.tostring(date_format)) + '}'

// @function Converts Pivot to a json string representation
// @param this Pivot
// @param date_format the format for Point bar_time (default: "y-MM-dd HH:mm")
// @returns string representation of Pivot
export method tostring(Pivot this, simple string date_format = "yyyy-MM-dd HH:mm") =>
	na(this)?'null':'{' + str.format('"point": {0}, "mode": {1}, "price_movement": {2}, "retracement_ratio": {3}', this.point.tostring(date_format), this.mode, this.price_movement, this.retracement_ratio) + '}'


// @function Converts Array of Pivots to a json string representation
// @param this Pivot array
// @param date_format the format for Point bar_time (default: "y-MM-dd HH:mm")
// @returns string representation of Pivot object array
export method tostring(array<Pivot> this, simple string date_format = "yyyy-MM-dd HH:mm") =>
    array<string> combinedStr = array.new<string>()
    if not na(this)
        for pivot in this
            combinedStr.push(pivot.tostring(date_format))
    '['+array.join(combinedStr, ",")+']'


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



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


//@function returns a color depending on given mode [-2,-1,1,2]
export method get_color(PivotColors this, int mode) =>
    switch mode
        2 => this.hh
        1 => this.lh
        -1 => this.hl
        -2 => this.ll
        => color.gray


//@function returns a LineArgs with line_color depending on given mode [-2,-1,1,2]
//@field this The PivotColors to chose from
//@field mode The selector variable
//@field base_line_args The LineArgs, holding the non color related settings
export method get_line_args(PivotColors this, int mode, D.LineArgs base_line_args = na) =>
    var default = D.LineArgs.new()
    args = na(base_line_args) ? default : base_line_args
    args.copy().set_line_color(this.get_color(mode))


//@function returns a LabelArgs with text_color and bg_color depending on given mode [-2,-1,1,2]
//@field this The PivotColors to chose from
//@field mode The selector variable
//@field base_label_args The LabelArgs, holding the non color related settings
//@field bg_colors A second color palette for the label background, if not set, label backgrounds will be transparent
export method get_label_args(PivotColors this, int mode, D.LabelArgs base_label_args = na, PivotColors bg_colors = na) =>
    var default = D.LabelArgs.new()
    args = na(base_label_args) ? default : base_label_args
    var transparent = PivotColors.new(#00000000, #00000000, #00000000, #00000000)
    bg_col = na(bg_colors) ? transparent : bg_colors
    args.copy().set_txt_color(this.get_color(mode)).set_bg_color(bg_col.get_color(mode)).set_style(math.sign(mode) == -1 ? label.style_label_up : label.style_label_down)


//@function returns a text depending on mode [-2,-1,1,2] => [LL,HL,LH,HH]
export method get_label_text(Pivot this) =>
    switch this.mode
        2 => 'HH'
        1 => 'LH'
        -1 => 'HL'
        -2 => 'LL'
        => ''


// @function mode contains infor about type of the Pivot (HH,LH,HL,LL), this function extracts only the direction (H/L)
// @param this Pivot
// @returns the signum of the mode (-1,1)
export method direction(Pivot this) =>
    na(this) ? 0 : int(math.sign(this.mode))


// @function mode contains infor about type of the Pivot (HH,LH,HL,LL), this function extracts only the direction (H/L)
// @param this Pivot
// @returns the signum of the mode (-1,1)
export method same_direction_as(Pivot this, Pivot other) =>
    na(this) or na(other) ? false : this.direction() == other.direction()


// @function multiplies pivot's price with it's direction to allow simplified comparison
// @returns product of direction and price level
method value(Pivot this) =>
    na(this) ? na : na(this.point) ? na : this.direction() * this.point.price


// @function check if a Pivot's price exceeds given price level. Works in both directions (Pivot's direction decides if it has to be greater or less than the price)
// @param this Pivot (gives us the pivot price level and direction)
// @param price The fix price level to exceed
// @returns true if exceeded, false otherwise
export method exceeds(Pivot this, float price) =>
    na(this) ? false : this.value() > this.direction() * price


// @function check if a Pivot's price level is exceeding the other Pivot's price level. Works in both directions (Pivot's direction decides if it has to be greater or less than the other)
// @param this Pivot (gives us the pivot price level and direction)
// @param other The other Pivot, whose price level that can be exceeded by this Pivot's price level
// @returns true if Pivot price level is exceeding the other Pivot's price level, false otherwise
export method exceeds(Pivot this, Pivot other) =>
    na(this) and na(other) ? false : na(other) ? true : na(this) ? false : this.same_direction_as(other) and this.value() > other.value()


// @function check if a Pivot's price is exceeded by given price level. Works in both directions (Pivot's direction decides if it has to be greater or less than the price)
// @param this Pivot (gives us the pivot price level and direction)
// @param price The fix price level that can exceed the Pivot price level
// @returns true if Pivot price level is exceeded by given price, false otherwise
export method exceeded_by(Pivot this, float price) =>
    na(this) ? true : this.value() <= this.direction() * price


// @function check if a Pivot's price level is exceeded by the other Pivot's price level. Works in both directions (Pivot's direction decides if it has to be greater or less than the other)
// @param this Pivot (gives us the pivot price level and direction)
// @param other The other Pivot, whose price level that can exceed this Pivot's price level
// @returns true if Pivot price level is exceeded by other Pivot's price level, false otherwise
export method exceeded_by(Pivot this, Pivot other) =>
    na(this) and na(other) ? false : na(this) ? true : na(other) ? false : this.same_direction_as(other) and this.value() <= other.value()


// @function check if a Pivot's price level is exceeded by any of the other Pivot's price levels. Works in both directions (Pivot's direction decides if it has to be greater or less than the other)
// @param this Pivot (gives us the pivot price level and direction)
// @param others The other Pivots, whose price levels can exceed this Pivot's price level
// @returns true if Pivot price level is exceeded by any other Pivot's price level, false otherwise
export method exceeded_by_any(Pivot this, Pivot[] others) =>
    exceeded = false
    if not na(this) 
        for p in others
            if this.exceeded_by(p)
                exceeded := true
                break
    exceeded

// @function calculates the retracement ration of this pivot compared to the move between two previous ones
// @param this Pivot gives us the current price
// @param sec_lastPivot The second to last Pivot, should be in same direction as this one
// @param lastPivot The last Pivot, should be in opposite direction to this one
// @returns a positive retracement_ratio of the retracement.
export method retracement_ratio(Pivot this, Pivot sec_lastPivot, Pivot lastPivot) =>
    float retracement_ratio = na(this) or na(lastPivot) or na(sec_lastPivot) ? na : math.round(PA.retracement_ratio(sec_lastPivot.point.price, lastPivot.point.price, this.point.price), 3)


// @function calculate the retracement price level that needs to be reached to achieve the target retracement_ratio
// @param this This pivot point, which is the latest fix pivot
// @param target_ratio The target retracement retracement_ratio (must be positive)
// @returns the retracement target retracement_ratio price level (float) or na if one of the inputs was na
export ratio_target(Pivot sec_lastPivot, Pivot lastPivot, float target_ratio) =>
    na(sec_lastPivot) or na(lastPivot) or na(target_ratio) ?  na : (lastPivot.point.price + target_ratio * (sec_lastPivot.point.price - lastPivot.point.price))


method rotate(D.Candle[] id, D.Candle item, int max) =>
    id.unshift(item)
    if id.size() > max
        id.pop()
    id

export method current_candle(HLData this) =>
    if na(this) ? true : na(this.candle_buffer) ? true : this.candle_buffer.size() == 0
        runtime.error('accessed HLData properties before first call to update()')
        na
    else
        this.candle_buffer.get(0)


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


/////////////////////////////////////////////////////////////
//#region         OBJECT UPDATE / update
/////////////////////////////////////////////////////////////

method init(HLData this) =>
    var initialized = false
    if not initialized
        if na(this.highest_candle)
            this.highest_candle := D.create_candle()
        if na(this.lowest_candle)
            this.lowest_candle := D.create_candle()
        if na(this.candle_buffer)
            this.candle_buffer := array.new<D.Candle>()
        initialized := true
    this

// @function fetches info about highest and lowest values within set length/period, also sets new_xxx flags to indicate conditions for new pivots or fractal confirmation
// @param this HLData
// @param ref_highest The series highest are calculated on, can be high or low
// @param ref_lowest The series lowest are calculated on, can be high or low
// @returns self with updated values
export method update(HLData this, float ref_highest = high, float ref_lowest = low) =>

    this.init()

    // offsets are negative, contrary to the offset we have in the ZigZag type and the one we use for the history indicator, so I'm converting this
	this.highest_offset := -ta.highestbars(ref_highest, this.length)
	this.lowest_offset := -ta.lowestbars(ref_lowest, this.length)
	this.highest := ta.highest(ref_highest, this.length)
	this.lowest := ta.lowest(ref_lowest, this.length)

    this.new_highest := this.highest_offset == 0
    this.new_lowest := this.lowest_offset == 0

    this.candle_buffer.rotate(D.Candle.new(time, time_close, bar_index, open, high, low, close, volume), this.length)
    
    if this.lowest_offset < this.candle_buffer.size()
        this.lowest_candle := this.candle_buffer.get(this.lowest_offset)
        this.lowest_point := na(this.lowest_point) ? chart.point.new(this.lowest_candle.bar_time, this.lowest_candle.bar_idx, this.lowest) : this.lowest_point.update(this.lowest_candle.bar_time, this.lowest_candle.bar_idx, this.lowest)
    if this.highest_offset < this.candle_buffer.size()
        this.highest_candle := this.candle_buffer.get(this.highest_offset)
        this.highest_point := na(this.highest_point) ? chart.point.new(this.highest_candle.bar_time, this.highest_candle.bar_idx, this.highest) : this.highest_point.update(this.highest_candle.bar_time, this.highest_candle.bar_idx, this.highest)

    var fractal_length = int(this.length / 2)
    // confirm point as fractal after half of the pivot period / length
    this.new_highest_fractal := this.highest_offset == fractal_length
    this.new_lowest_fractal := this.lowest_offset == fractal_length

    this

//@function updates this Pivot's (prev, xloc, price_movement, retracement_ratio and mode) compared to another previous Pivot
export method update_relation(Pivot this, Pivot prev) =>
    if not na(this) and not na(prev)
        this.prev := prev
        this.price_movement := this.point.price - prev.point.price

        direction = int(math.sign(this.price_movement))
        if direction == 0
            direction := prev.direction() != 0 ? -prev.direction() : 1

        retracement_ratio_rel = prev.price_movement != 0 ? this.price_movement / prev.price_movement : 0 // avoiding division by 0
        this.retracement_ratio := -retracement_ratio_rel
        hhll = math.abs(direction * -retracement_ratio_rel) > 1
        this.mode := int(direction * (hhll ? 2 : 1))
    this

//@function updates this Pivot's Point coordinates and optional relation to its previous pivot Point
export method update(Pivot this, int bar_time, int bar_idx, float price, Pivot prev = na, D.Candle candle = na) =>
    if not na(this)
        if na(this.point)
            this.point := chart.point.new(bar_time, bar_idx, price)
        this.point.update(bar_time, bar_idx, price)

        if not na(prev)
            this.update_relation(prev)
        else // initial just go with bullish
            this.price_movement := price - this.point.price
            this.mode := int(math.sign(this.price_movement))

        if not na(candle)
            this.candle := candle
    this

export method update(Pivot this, chart.point update, Pivot prev = na, D.Candle candle = na) =>
    this.update(update.time, update.index, update.price, prev, candle)

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


/////////////////////////////////////////////////////////////
//#region         OBJECT CREATION / create
/////////////////////////////////////////////////////////////

export method create_next(Pivot this, int bar_time, int bar_idx, float price, D.Candle candle = na) =>
    Pivot.new().update(bar_time, bar_idx, price, this, candle)

export method create_next(Pivot this, chart.point update, D.Candle candle = na) =>
    Pivot.new().update(update, this, candle)

//@function converts Point to Pivot, set it's properties in relation to previously added Pivots (prev, xloc, price_movement, retracement_ratio and mode) and adds it to the array
export method add_next(Pivot[] id, chart.point point, int max = 10, D.Candle candle = na) =>
    if not na(id)

        if id.size() > 0
            prev = id.get(0)
            next = prev.create_next(point, candle)
            if prev.direction() == next.direction()
                runtime.error(str.format('Next pivot {0} has the same direction as the previous {1}, use update_last instead of add_next with a new one', next.tostring(), prev.tostring()))
            id.unshift(next)
        else
            id.unshift(Pivot.new().update(point, candle = candle))
            
        if id.size() > max
            id.pop()
    id

//@function updates latest Pivot in array, in case it's exceeded by the update
export method update_last(Pivot[] id, chart.point update, D.Candle candle = na) =>
    if not na(id) and not na(update)
        if id.size() > 0
            last = id.get(0)
            if last.exceeded_by(update.price)
                last.update(update, na(last.prev) ? Pivot.new(last.point.copy()) : last.prev, candle)
    id

//@function updates latest Pivot in array, in case it's exceeded by the update price in same direction, else it adds a new Pivot in opposite direciton
export method update_last_or_add_next(Pivot[] id, chart.point point, int max = 10, D.Candle candle = na) =>
    if not na(id) and not na(point)
        if id.size() > 0
            Pivot last = id.get(0)
            if last.exceeded_by(point.price)
                last.update(point, na(last.prev) ? Pivot.new(last.point.copy()) : last.prev, candle)
            else
                id.add_next(point, max, candle)
        else
            id.add_next(point, max, candle)
    id






/////////////////////////////////////////////////////////////
//#endregion         OBJECT CREATION / create
/////////////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////
//#region                   TEST
/////////////////////////////////////////////////////////////

import robbatt/lib_log/6 as LOG
import robbatt/lib_unit/6 as UNIT

var logger = LOG.Logger.new(min_level = 0, color_logs = true, display = LOG.LogDisplay.new(6, position.bottom_left))
var assert = UNIT.Test.new(strict = false, verbose = true, logger = logger)

if bar_index == 20000
    // regular move
    p0 = Pivot.new(chart.point.new(time[5], bar_index[5], 150))
    logger.debug(p0.tostring())
    p1 = p0.create_next(time[4], bar_index[4], 100)
    logger.debug(p1.tostring())
    assert.equals(p1.retracement_ratio, 0, 'expect a 0 retracement ratio after first continuation from 150 to 100')
    assert.equals(p1.mode, -1, 'expect mode -1 L after first continuation from 150 to 100')
    assert.equals(p1.price_movement, -50, 'expect -50 price move after first continuation from 150 to 100')
    
    p2 = p1.create_next(time[3], bar_index[3], 200)
    logger.debug(p2.tostring())
    assert.equals(p2.retracement_ratio, 2, 'expect 200 to be a 2 retracement ratio after first trend change from 150 to 100 to 200')
    assert.equals(p2.mode, 2, 'expect 200 to be a mode 2 HH after a move from 150 to 100 to 200')
    assert.equals(p2.price_movement, 100, 'expect 200 to be a 100 price move after a move from 150 to 100 to 200')
    
    p3 = Pivot.new(chart.point.new(time[3], bar_index[3], 1000)).update(time[2], bar_index[2], 150, p2)
    logger.debug(p3.tostring())
    assert.equals(p3.retracement_ratio, 0.5, 'expect 150 to be a 0.5 retracement ratio after a move from 100 to 200')
    assert.equals(p3.mode, -1, 'expect 150 to be a mode -1 HL after a move from 100 to 200')
    assert.equals(p3.price_movement, -50, 'expect 150 to be a -50 price move after a move from 100 to 200')

    // continuation bearish
    p4 = Pivot.new().update(time[1], bar_index[1], 0, p2)
    logger.debug(p4.tostring())
    assert.equals(p4.retracement_ratio, 2, 'expect 0 to be a 2 retracement ratio after a move from 100 to 200')
    assert.equals(p4.mode, -2, 'expect 0 to be a mode -2 LL after a move from 100 to 200')
    assert.equals(p4.price_movement, -200, 'expect 0 to be a -200 price move after a move from 100 to 200')


    // continuation bearish
    p5 = Pivot.new().update(time, bar_index, 100, p4)
    logger.debug(p5.tostring())
    assert.equals(p5.retracement_ratio, 0.5, 'expect 100 to be a 0.5 retracement ratio after a move from 200 to 0')
    assert.equals(p5.mode, 1, 'expect 100 to be a mode 1 LH after a move from 200 to 0')
    assert.equals(p5.price_movement, 100, 'expect 100 to be a +100 price move after a move from 200 to 0')


/////////////////////////////////////////////////////////////
//#endregion                   TEST
/////////////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////
//#region                   DEMO
/////////////////////////////////////////////////////////////


var HLData pd = HLData.new(length = 5)
var int fractal_offset = 2 // int(pd.length / 2)

pd.update(high, low)

int max_pivots = 5
var Pivot[] pivots = array.new<Pivot>()
var D.Label[] labels = array.new<D.Label>()
var D.Line[] lines = array.new<D.Line>()
var D.Candle curr_candle = D.create_candle()
curr_candle.update()

i_add_pivot_at_intermediate_fractal_when_force_adding_pivot = input.bool(true, 'add_pivot_at_intermediate_fractal_when_force_adding_pivot')
i_ignore_double_pivots_when_candle_closes_in_trend_direction = input.bool(false, 'ignore_double_pivots_when_candle_closes_in_trend_direction')
bool double_pivot = false
bool extra_pivot = false
bool direction_change = false

if pivots.size() < 2
    if pd.new_highest
        pivots.add_next(pd.highest_point, max_pivots)
    if pd.new_lowest
        pivots.add_next(pd.lowest_point, max_pivots)
else
    Pivot sec_last = pivots.get(1)
    Pivot last = pivots.get(0)
    int trend = last.direction()

    int bars_since_last = bar_index - last.point.index
    int bars_since_sec_last = na(sec_last) ? na : bar_index - sec_last.point.index
    bool candle_in_trend_direction = trend == curr_candle.direction()

    add_pivot_at_intermediate_fractal_when_force_adding_pivot = i_add_pivot_at_intermediate_fractal_when_force_adding_pivot // changes algorithm to add extra pivots at fractal points when a forced pivot is added
    ignore_double_pivots_when_candle_closes_in_trend_direction = i_ignore_double_pivots_when_candle_closes_in_trend_direction // changes algorithm to ignore double pivots, when their candle closes in trend direction
    double_pivot := pd.new_highest and pd.new_lowest and (not ignore_double_pivots_when_candle_closes_in_trend_direction or not candle_in_trend_direction and barstate.isconfirmed)

    if trend == 1
        // force low (sideways, lower highs, no new lowest for trend change)
        forced_l = not pd.new_lowest and bars_since_last >= pd.length and not last.exceeded_by(pd.highest) and (add_pivot_at_intermediate_fractal_when_force_adding_pivot or pd.new_highest)
        if forced_l
            pivots.add_next(pd.lowest_point, max_pivots)
            direction_change := true

        // trend continuation
        if pd.new_highest
            if forced_l and add_pivot_at_intermediate_fractal_when_force_adding_pivot
                pivots.add_next(pd.highest_point, max_pivots)
                extra_pivot := true
            // trend continuation
            else if last.exceeded_by(pd.highest)
                pivots.update_last(pd.highest_point)


        if  pd.new_lowest
            // double pivot
            if double_pivot
                pivots.add_next(pd.lowest_point, max_pivots)
                direction_change := true
                double_pivot := true
            // trend change
            else if pd.highest_candle.l > pd.lowest
                pivots.add_next(pd.lowest_point, max_pivots)
                direction_change := true


    if trend == -1
        // force high (sideways, higher lows, no new highest for trend change)
        forced_h = not pd.new_highest and bars_since_last >= pd.length and not last.exceeded_by(pd.lowest) and (add_pivot_at_intermediate_fractal_when_force_adding_pivot or pd.new_lowest)
        if forced_h
            pivots.add_next(pd.highest_point, max_pivots)
            direction_change := true

        if pd.new_lowest
            // trend change after forced h
            if forced_h and add_pivot_at_intermediate_fractal_when_force_adding_pivot
                pivots.add_next(pd.lowest_point, max_pivots)
                extra_pivot := true
            // trend continuation
            else if last.exceeded_by(pd.lowest)
                pivots.update_last(pd.lowest_point)

        if pd.new_highest
            // double pivot
            if double_pivot
                pivots.add_next(pd.highest_point, max_pivots)
                direction_change := true
                double_pivot := true
            // trend change
            else if pd.lowest_candle.h < pd.highest
                pivots.add_next(pd.highest_point, max_pivots)
                direction_change := true


// update vars for drawing
count = pivots.size()
live = input.bool(true, 'live plot')
var _colors = PivotColors.new()

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

    if live
        var D.Line current_line = sec_last.point.create_line(last.point).draw()
        var D.Label current_label = last.point.create_label(last.get_label_text()).draw()

        // finish previous line and label before switching to double/extra pivot
        if double_pivot or extra_pivot
            pivot = extra_pivot ? thr_last : sec_last
            current_label.txt := pivot.get_label_text()
            current_label.apply_style(_colors.get_label_args(pivot.mode)).draw()
            current_line.apply_style(_colors.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 direction_change
            current_line.plot.copy().set_style(line.style_solid) // previous pivot is confirmed
            current_label.plot.copy()
        
        // draw intermediate
        if extra_pivot
            current_line.start := thr_last.point
            current_line.end := sec_last.point
            current_line.apply_style(_colors.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(_colors.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.apply_style(_colors.get_line_args(last.mode).set_style(line.style_dashed)).draw(extend_only = false) // new pivot is temporary, make line dashed
        current_label.apply_style(_colors.get_label_args(last.mode)).draw()

    else if double_pivot or extra_pivot or direction_change
        thr_last.point.create_line(sec_last.point).draw(extend_only = false).apply_style(_colors.get_line_args(sec_last.mode))
        sec_last.point.create_label(sec_last.get_label_text()).draw().apply_style(_colors.get_label_args(sec_last.mode))
        

// DEBUG
plotshape(pd.new_highest, 'new highest', shape.triangledown, location.abovebar, color.purple, text = 'new highest', size = size.small, display = display.pane + display.data_window)
plotshape(pd.new_lowest, 'new lowest', shape.triangleup, location.belowbar, color.blue, text = 'new lowest', size = size.small, display = display.pane + display.data_window)
plotshape(pd.new_highest_fractal, 'highest fractal', shape.triangledown, location.abovebar, color.red, size = size.tiny, offset = -fractal_offset, display = display.pane + display.data_window)
plotshape(pd.new_lowest_fractal, 'lowest fractal', shape.triangleup, location.belowbar, color.green, size = size.tiny, offset = -fractal_offset, display = display.pane + display.data_window)
plotshape(pd.new_highest_fractal, 'new highest fractal', shape.diamond, location.abovebar, color.red, text = 'fractal\nconfirmed', size = size.tiny, display = display.pane + display.data_window)
plotshape(pd.new_lowest_fractal, 'new lowest fractal', shape.diamond, location.belowbar, color.green, text = 'fractal\nconfirmed', size = size.tiny, display = display.pane + display.data_window)

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