Broadening Formations [TFO]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © tradeforopp

//@version=5
indicator("Broadening Formations [TFO]", overlay = true, max_lines_count = 500)


// -------------------- Inputs --------------------
var g1 = "Settings"
show_pivots = input.bool(true, "Show Fractal Pivots", group = g1)
show_tl_test = input.bool(true, "Show Deviations from Range", group = g1)
delete_old = input.bool(false, "Delete Old Trendlines", group = g1)
pivot_strength = input.int(20, minval = 1, maxval = 50, title = "Pivot Strength", tooltip = "Determines what fractal length is used for highs and lows", group = g1)

var g2 = "Style"
bf_color = input.color(color.gray, "Initial Color", group = g2)
deviation_color = input.color(color.red, "Deviation Color", group = g2)
line_style = input.string(defval = 'Solid', title = "Line Style", inline = "Pivot Style", options = ['Solid', 'Dotted', 'Dashed'], group = g2)
line_width = input.int(defval = 1, minval = 1, maxval = 5, title = "", inline = "Pivot Style", group = g2)
// -------------------- Inputs --------------------


// -------------------- Variables --------------------
fractal_idx = pivot_strength
max_candles_ago = 100
max_array_size = math.floor(max_candles_ago / 2)
// -------------------- Variables --------------------


// -------------------- Initializing --------------------
var pivot_lows = array.new_float(0)
var pivot_lows_idx = array.new_int(0)

var pivot_highs = array.new_float(0)
var pivot_highs_idx = array.new_int(0)

var line upper_tl = na
var line lower_tl = na
// -------------------- Initializing --------------------


// -------------------- Functions --------------------
get_line_type(style) =>
    result = switch style
        'Solid' => line.style_solid
        'Dotted' => line.style_dotted
        'Dashed' => line.style_dashed
    result

fractal_high(i) =>
    accumulator = true
    for j = -i to i
        if j != 0
            accumulator := accumulator and high[i] > high[i + j]
    result = accumulator

fractal_low(i) => 
    accumulator = true
    for j = -i to i
        if j != 0
            accumulator := accumulator and low[i] < low[i + j]
    result = accumulator

append_array(a, x) =>
    array.unshift(a, x)
    if array.size(a) > max_array_size
        array.pop(a)
// -------------------- Functions --------------------


// -------------------- Test Fractal Low/High --------------------
line_style := get_line_type(line_style)

is_fractal_high = fractal_high(fractal_idx)
is_fractal_low = fractal_low(fractal_idx)

new_low = false
new_high = false

if is_fractal_low
    append_array(pivot_lows, low[fractal_idx])
    append_array(pivot_lows_idx, fractal_idx)
    
if is_fractal_high
    append_array(pivot_highs, high[fractal_idx])
    append_array(pivot_highs_idx, fractal_idx)

plotshape(show_pivots and is_fractal_high, style = shape.circle, color = bf_color, location = location.abovebar, offset = -fractal_idx, size = size.tiny)
plotshape(show_pivots and is_fractal_low, style = shape.circle, color = bf_color, location = location.belowbar, offset = -fractal_idx, size = size.tiny) 
// -------------------- Test Fractal Low/High --------------------


// -------------------- Plot Trendlines --------------------
if array.size(pivot_highs) > 1 and array.size(pivot_lows) > 1
    low_new = array.get(pivot_lows, 0)
    low_old = array.get(pivot_lows, 1)

    low_new_idx = array.get(pivot_lows_idx, 0)
    low_old_idx = array.get(pivot_lows_idx, 1)

    high_new = array.get(pivot_highs, 0)
    high_old = array.get(pivot_highs, 1)

    high_new_idx = array.get(pivot_highs_idx, 0)
    high_old_idx = array.get(pivot_highs_idx, 1)

    if low_new < low_old
        x1 = bar_index - low_old_idx
        x2 = bar_index - fractal_idx
        y1 = low_old
        y2 = low_new

        slope = (y2 - y1) / (x2 - x1)

        valid_tl = true
        for i = 0 to x2 - x1
            curr_y = slope * i + low_old
            if low[x2 - x1 + fractal_idx - i] < curr_y
                valid_tl := false
                break

        if valid_tl
            if not na(lower_tl) and delete_old
                line.delete(lower_tl)

            offset = slope * fractal_idx
            lower_tl := line.new(bar_index - low_old_idx, low_old, bar_index - low_new_idx, low_new, style = line_style, color = bf_color, width = line_width)
            array.set(pivot_lows, 0, na)
            array.set(pivot_lows, 1, na)
        
    if high_new > high_old
        x1 = bar_index - high_old_idx
        x2 = bar_index - fractal_idx
        y1 = high_old
        y2 = high_new

        slope = (y2 - y1) / (x2 - x1)

        valid_tl = true
        for i = 0 to x2 - x1
            curr_y = slope * i + high_old
            if high[x2 - x1 + fractal_idx - i] > curr_y
                valid_tl := false
                break

        if valid_tl
            if not na(upper_tl) and delete_old
                line.delete(upper_tl)

            offset = slope * fractal_idx
            upper_tl := line.new(bar_index - high_old_idx, high_old, bar_index - high_new_idx, high_new, style = line_style, color = bf_color, width = line_width)
            array.set(pivot_highs, 0, na)
            array.set(pivot_highs, 1, na)
// -------------------- Plot Trendlines --------------------


// -------------------- Deviation From Range --------------------
upper_test = false
lower_test = false

if not na(upper_tl)
    x1 = line.get_x1(upper_tl)
    x2 = line.get_x2(upper_tl)
    y1 = line.get_y1(upper_tl)
    y2 = line.get_y2(upper_tl)

    slope = (y2 - y1) / (x2 - x1)

    new_y = y2 + slope * (bar_index - x2)

    if high > new_y
        if delete_old
            line.delete(upper_tl)
        else
            line.set_x2(upper_tl, bar_index)
            line.set_y2(upper_tl, new_y)
            line.set_color(upper_tl, deviation_color)
            upper_tl := na
        upper_test := true

if not na(lower_tl)
    x1 = line.get_x1(lower_tl)
    x2 = line.get_x2(lower_tl)
    y1 = line.get_y1(lower_tl)
    y2 = line.get_y2(lower_tl)

    slope = (y2 - y1) / (x2 - x1)

    new_y = y2 + slope * (bar_index - x2)

    if low < new_y
        if delete_old
            line.delete(lower_tl)
        else
            line.set_x2(lower_tl, bar_index)
            line.set_y2(lower_tl, new_y)
            line.set_color(lower_tl, deviation_color)
            lower_tl := na
        lower_test := true

plotshape(show_tl_test and upper_test, style = shape.triangledown, color = deviation_color, size = size.small, location = location.abovebar)
plotshape(show_tl_test and lower_test, style = shape.triangleup, color = deviation_color, size = size.small, location = location.belowbar)

alertcondition(upper_test, "Upper Trendline Test")
alertcondition(lower_test, "Lower Trendline Test")
// -------------------- Deviation From Range --------------------


// -------------------- Fractal Low --------------------
if array.size(pivot_lows) > 0
    for i = 0 to array.size(pivot_lows) - 1
        low_idx = array.get(pivot_lows_idx, i)
        // Increase every pivot bar index by 1
        array.set(pivot_lows_idx, i, low_idx + 1)
// -------------------- Fractal Low --------------------


// -------------------- Fractal High --------------------
if array.size(pivot_highs) > 0
    for i = 0 to array.size(pivot_highs) - 1
        high_idx = array.get(pivot_highs_idx, i)
        // Increase every pivot bar index by 1
        array.set(pivot_highs_idx, i, high_idx + 1)
// -------------------- Fractal High --------------------