Swing Top / Bottom Finder
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © zathruswriter

//@version=5
indicator("Swing Top / Bottom Finder", overlay = true, max_bars_back = 4900, max_boxes_count = 500, max_labels_count = 500, max_lines_count = 500)

GROUP_GENERAL = "General Settings"
GROUP_ADVANCED = "Advanced Settings"
GROUP_VISUAL = "Visual Settings"

search_threshold = input.float( 0.04, "Search Threshold %", step = 0.01, group = GROUP_GENERAL, tooltip = "How many % around our currently checked high/low should we consider when checking for similar swing highs and lows in the past." )
lookback_bars = input.int( 500, "Lookback length (bars)", group = GROUP_GENERAL, tooltip = "Number of bars in the past up to where highs and lows are being checked for similarities with the current high/low." )
min_prev_bottoms = input.int( 3, "Min previous horizontals with same high/low", group = GROUP_GENERAL, tooltip = "A swing top/bottom must be confirmed by at least this many times in the past to be considered valid.")
min_diff_between_highs_lows = input.float( 0.17, "Min difference between two highs/lows %", step = 0.01, group = GROUP_GENERAL, tooltip = "This setting will tell us how much apart will two similar highs/lows be in order to be counted and displayed as different highs/lows." )
bars_wait_before_next_swing = input.int( 17, "Pause after a high/low is found (bars)", group = GROUP_GENERAL, tooltip = "Sometimes, there could be many highs/lows with a very simmilar price tag around the same area. This setting allows waiting for X amount of bars before searching for the next one, eliminating this crowd effect." )

bars_wait_before_next_swing_potential = input.int( 3, "Pause after a potential high/low is found (bars)", group = GROUP_ADVANCED )
check_only_current_swing_candles = input.bool( true, "Perform checks on current swing high/low candles ONLY", group = GROUP_ADVANCED, tooltip = "When ticked, only candles that are actually swing highs/lows will be checked for validity (e.g. against other similar previous highs/lows). Otherwise, all candles will be checked." )
show_potential_swings = input.bool( true, "Show potential swing highs/lows", group = GROUP_ADVANCED, tooltip = "When ticked, potential swing highs/lows will be shown even when no previous high/low exists in the history for that price level yet." )

show_lines = input.bool( true, "Show high/low lines", group = GROUP_VISUAL, tooltip = "High/low lines serve the purpose of showing you the last unbroken swing high/low. This could be used to observe a potential TP level during scalping." )
i_trend_line_color = input.color( color.black, "Trend Line Color", group = GROUP_VISUAL, inline = "tl" )
i_trend_line_width = input.int( 2, "width", group = GROUP_VISUAL, inline = "tl" )
i_trend_line_style = input.string( line.style_solid, "style", options = [line.style_solid, line.style_dashed, line.style_dotted], group = GROUP_VISUAL, inline = "tl" )


// VARIABLES
transparentcolor = color.new(color.white,100)
var last_low_point = -1.0
var last_high_point = -1.0
var last_low_point_potential = -1.0
var last_high_point_potential = -1.0
var paused_highs = false
var paused_lows = false
var paused_highs_potential = false
var paused_lows_potential = false
var pause_counter_highs = 0
var pause_counter_lows = 0
var pause_counter_highs_potential = 0
var pause_counter_lows_potential = 0
var swing_high_line = line.new( bar_index, 0, bar_index, 0, color = transparentcolor, width = i_trend_line_width, style = i_trend_line_style )
var swing_low_line = line.new( bar_index, 0, bar_index, 0, color = transparentcolor, width = i_trend_line_width, style = i_trend_line_style )


// FUNCTIONS
s_( txt ) =>
    str.tostring( txt )

f_debug_label( txt ) =>
    label.new(bar_index, high, str.tostring( txt ), color = color.lime, textcolor = color.black)

f_debug_label_down( txt ) =>
    label.new(bar_index, low, str.tostring( txt ), color = color.lime, textcolor = color.black, style = label.style_label_up)

f_set_swing_line_to_current_bar( which, offset ) =>
    if show_lines
        //f_debug_label_down( which + " / " + s_( bar_index ) + " / " + s_( offset ) )
        // f_debug_label( s_( bar_index[ offset ] ) + " / " + s_( bar_index - offset ) )
        //f_debug_label( s_( bar_index - offset) + " / " + s_( which == "high" ? last_high_point > last_high_point_potential ? last_high_point : last_high_point_potential : last_low_point > last_low_point_potential ? last_low_point : last_low_point_potential ) )
        //f_debug_label( s_( last_low_point ) + " / " + s_( last_low_point_potential ) )
        swing_line = which == "high" ? swing_high_line : swing_low_line
        line.set_x1( swing_line, bar_index - offset )
        line.set_x2( swing_line, bar_index - offset )
        line.set_y1( swing_line, which == "high" ? last_high_point > last_high_point_potential ? last_high_point : last_high_point_potential : last_low_point > last_low_point_potential ? last_low_point : last_low_point_potential )
        line.set_y2( swing_line, which == "high" ? last_high_point > last_high_point_potential ? last_high_point : last_high_point_potential : last_low_point > last_low_point_potential ? last_low_point : last_low_point_potential )
        line.set_color( swing_line, i_trend_line_color )

f_extend_swing_line_to_current_bar( which ) =>
    if show_lines
        swing_line = which == "high" ? swing_high_line : swing_low_line
        line.set_x2( swing_line, bar_index )

f_reset_swing_line( which ) =>
    if show_lines
        swing_line = which == "high" ? swing_high_line : swing_low_line
        line.set_x1( swing_line, bar_index )
        line.set_x2( swing_line, bar_index )
        line.set_y1( swing_line, 0 )
        line.set_y2( swing_line, 0 )
        line.set_color( swing_line, transparentcolor )

f_is_swing_line_reset( which ) =>
    swing_line = which == "high" ? swing_high_line : swing_low_line
    line.get_y1( swing_line ) == 0

// LOGIC
num_bottoms = 0
num_tops = 0

// if we enabled checking of swing candles only, activate offset here
last_candle_was_swing = check_only_current_swing_candles ? false : true
last_candle_swing_type = ""
check_offset = check_only_current_swing_candles ? 1 : 0
is_unmarked_swing_top = false
is_unmarked_swing_bot = false

// also, check for last candle as swing candle
if check_only_current_swing_candles
    if high < high[ check_offset ] and high[ check_offset + 1] < high[ check_offset ]
        last_candle_was_swing := true
        last_candle_swing_type := "high"

    if low > low[ check_offset ] and low[ check_offset + 1] > low[ check_offset ]
        last_candle_was_swing := true
        last_candle_swing_type := "low"

// even if swing candles checking is disabled, this variable will still be set to true
if last_candle_was_swing
    if ( paused_lows == false or ( check_only_current_swing_candles and paused_lows_potential == false ) ) and ( check_only_current_swing_candles == false or last_candle_swing_type == "low" )
        // find swing bottoms if this low is not too close to the previous swing low found
        if last_low_point == -1.0 or math.abs( ( ( low[ check_offset ] - last_low_point ) / last_low_point ) * 100 ) >= min_diff_between_highs_lows
            //f_debug_label( str.tostring( low ) + " / " + str.tostring( last_low_point ) + "\n" + str.tostring( math.abs( ( ( low - last_low_point ) / last_low_point ) * 100 ) ) + " / " + s_( min_diff_between_highs_lows ) )
            for i = 0 to lookback_bars
                low_with_threshold = ( ( low[ i + check_offset ] / 100 ) * search_threshold )
                point_was_swing_low = false

                if i > 0
                    point_was_swing_low := low[ i + check_offset - 1 ] > low[ i + check_offset ] and low[ i + check_offset + 1 ] > low[ i + check_offset ]

                if paused_lows == false and point_was_swing_low and low[ check_offset ] < low[ i + check_offset ] + low_with_threshold and low[ check_offset ] > low[ i + check_offset ] - low_with_threshold
                    num_bottoms := num_bottoms + 1

            if num_bottoms >= min_prev_bottoms
                last_low_point := low[ check_offset ]
            else if paused_lows_potential == false and low > low[ check_offset ] and low[ check_offset + 1] > low[ check_offset ]
                is_unmarked_swing_bot := true
                last_low_point_potential := low[ check_offset ]
    else
        f_extend_swing_line_to_current_bar( "low" )

        if pause_counter_lows >= bars_wait_before_next_swing
            pause_counter_lows := 0
            paused_lows := false
        else
            pause_counter_lows := pause_counter_lows + 1

        if pause_counter_lows_potential >= bars_wait_before_next_swing_potential
            pause_counter_lows_potential := 0
            paused_lows_potential := false
        else
            pause_counter_lows_potential := pause_counter_lows_potential + 1

    if ( paused_highs == false or ( check_only_current_swing_candles and paused_highs_potential == false ) ) and ( check_only_current_swing_candles == false or last_candle_swing_type == "high" )
        // find swing tops if this high is not too close to the previous swing high found
        if last_high_point == -1.0 or math.abs( ( ( high[ check_offset ] - last_high_point ) / last_high_point ) * 100 ) >= min_diff_between_highs_lows
            for i = 0 to lookback_bars
                high_with_threshold = ( ( high[ i + check_offset ] / 100 ) * search_threshold )
                point_was_swing_high = false

                if i > 0
                    point_was_swing_high := high[ i + check_offset - 1 ] < high[ i + check_offset ] and high[ i + check_offset + 1 ] < high[ i + check_offset ]

                if paused_highs == false and point_was_swing_high and high[ check_offset ] < high[ i + check_offset ] + high_with_threshold and high[ check_offset ] > high[ i + check_offset ] - high_with_threshold
                    num_tops := num_tops + 1

            if num_tops >= min_prev_bottoms
                last_high_point := high[ check_offset ]
            else if paused_highs_potential == false and high < high[ check_offset ] and high[ check_offset + 1] < high[ check_offset ]
                is_unmarked_swing_top := true
                last_high_point_potential := high[ check_offset ]
    else
        f_extend_swing_line_to_current_bar( "high" )

        if pause_counter_highs >= bars_wait_before_next_swing
            pause_counter_highs := 0
            paused_highs := false
        else
            pause_counter_highs := pause_counter_highs + 1

        if pause_counter_highs_potential >= bars_wait_before_next_swing_potential
            pause_counter_highs_potential := 0
            paused_highs_potential := false
        else
            pause_counter_highs_potential := pause_counter_highs_potential + 1

if num_bottoms >= min_prev_bottoms
    // pause until the number of new bars is reached
    paused_lows := true
    last_low_point_potential := -1
    // also activate the low point line
    if f_is_swing_line_reset( "low" )
        f_set_swing_line_to_current_bar( "low", check_offset )
    f_extend_swing_line_to_current_bar( "low" )

if is_unmarked_swing_bot
    // pause until the number of new bars is reached
    paused_lows_potential := true
    last_low_point := -1
    // also activate the low point line
    if f_is_swing_line_reset( "low" )
        f_set_swing_line_to_current_bar( "low", check_offset )
    f_extend_swing_line_to_current_bar( "low" )

if num_tops >= min_prev_bottoms
    // pause until the number of new bars is reached
    paused_highs := true
    // also activate the high point line
    if f_is_swing_line_reset( "high" )
        f_set_swing_line_to_current_bar( "high", check_offset )
    f_extend_swing_line_to_current_bar( "high" )

if is_unmarked_swing_top
    // pause until the number of new bars is reached
    paused_highs_potential := true
    // also activate the high point line
    if f_is_swing_line_reset( "high" )
        f_set_swing_line_to_current_bar( "high", check_offset )
    f_extend_swing_line_to_current_bar( "high" )


// PLOTTING
plotshape( num_bottoms >= min_prev_bottoms ? 1 : na, "Swing Low", shape.triangleup, location.belowbar, color.green, size = size.small, offset = -check_offset )
plotshape( num_tops >= min_prev_bottoms ? 1 : na, "Swing High", shape.triangledown, location.abovebar, color.red, size = size.small, offset = -check_offset )
plotshape( show_potential_swings and is_unmarked_swing_bot ? 1 : na, "Potential Swing Low", shape.triangleup, location.belowbar, color.aqua, size = size.tiny, offset = -check_offset )
plotshape( show_potential_swings and is_unmarked_swing_top ? 1 : na, "Potential Swing High", shape.triangledown, location.abovebar, color.fuchsia, size = size.tiny, offset = -check_offset )


// reset or update swing lines
high_line_was_reset = false
low_line_was_reset = false

if ( last_high_point > last_high_point_potential and high < last_high_point ) or ( last_high_point < last_high_point_potential and high < last_high_point_potential )
    f_extend_swing_line_to_current_bar( "high" )
else if ( last_high_point > last_high_point_potential and high >= last_high_point ) or ( last_high_point < last_high_point_potential and high >= last_high_point_potential )
    if f_is_swing_line_reset( "high" ) == false
        f_reset_swing_line( "high" )
        high_line_was_reset := true

if ( last_low_point > last_low_point_potential and low > last_low_point ) or ( last_low_point < last_low_point_potential and low > last_low_point_potential )
    f_extend_swing_line_to_current_bar( "low" )
else if ( last_low_point > last_low_point_potential and low <= last_low_point ) or ( last_low_point < last_low_point_potential and low <= last_low_point_potential )
    if f_is_swing_line_reset( "low" ) == false
        f_reset_swing_line( "low" )
        low_line_was_reset := true


// ALERTS
alertcondition( num_bottoms >= min_prev_bottoms, "Swing Low Found", "Swing Low Found" )
alertcondition( num_tops >= min_prev_bottoms, "Swing High Found", "Swing High Found" )
alertcondition( show_potential_swings and is_unmarked_swing_bot, "Potential Swing Low Found", "Potential Swing Low Found" )
alertcondition( show_potential_swings and is_unmarked_swing_top, "Potential Swing High Found", "Potential Swing High Found" )
alertcondition( high_line_was_reset, "Swing High Level Broken", "Swing High Level Broken" )
alertcondition( low_line_was_reset, "Swing Low Level Broken", "Swing High Level Broken" )

// provide a one to serve them all option as well
if num_bottoms >= min_prev_bottoms
    alert( "Swing Low Found", alert.freq_once_per_bar )

if num_tops >= min_prev_bottoms
    alert( "Swing High Found", alert.freq_once_per_bar )

if show_potential_swings and is_unmarked_swing_bot
    alert( "Potential Swing Low Found", alert.freq_once_per_bar )

if show_potential_swings and is_unmarked_swing_top
    alert( "Potential Swing High Found", alert.freq_once_per_bar )

if show_lines and high_line_was_reset
    alert( "Swing High Level Broken", alert.freq_once_per_bar )

if show_lines and low_line_was_reset
    alert( "Swing Low Level Broken", alert.freq_once_per_bar )