NormInvTargetSeeker
// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// @ RickSimpson

//@version=5
indicator('NormInvTargetSeeker', overlay = true, max_bars_back = 500, max_lines_count = 500, max_boxes_count = 500, max_labels_count = 500)

// ◀─── User paramaters ───►

i_labels                = input.bool(true,                     'Inverse Normalized Performance Labels',                                  group = 'Inverse Normalized Performance')
i_out_css               = input.color(color.new(#07A896, 0), 'Outperformance',                                                         group = 'Inverse Normalized Performance', inline  = 'Norminv css')
i_und_css               = input.color(color.new(#E81E1E, 0), 'Underperformance',                                                       group = 'Inverse Normalized Performance', inline  = 'Norminv css')
i_multitimeframe_filter = input.bool(false,                    'Harmonized Multi-Timeframe Analysis',                                    group = 'Inverse Normalized Performance', tooltip = 'When enabled, this feature analyzes signals across all timeframes to ensure consistency and reliability before presenting a valid signal. This approach aims to enhance the robustness of trend indications.')
i_bands                 = input.bool(false,                    'Display Statistical Range Bands',                                        group = 'Inverse Normalized Performance', tooltip = 'Enable to show bands representing statistical price range deviations. Useful for identifying volatility and market extremes.')
i_sr                    = input.bool(true,                     'Support & Resistance Line',                                              group = 'Inverse Normalized Performance', tooltip = 'Toggle to display or hide the Outperformance/Underperformance Lines on the chart. These lines can provide insights into potential support or resistance zones.')
i_sr_lb                 = input.bool(true,                     'Line Price Levels',                                                      group = 'Inverse Normalized Performance', tooltip = 'Toggle to display or hide the price level next to the Outperformance/Underperformance Lines. This provides an exact price level for the support or resistance.')
i_layout                = input.string('Wick',                 'Line Type',                 options = ['Wick', 'Average', 'Full Range'], group = 'Inverse Normalized Performance', tooltip = 'Select the style of the Outperformance/Underperformance Lines. "Wick" uses candle wicks, "Average" displays an average range, and "Full Range" displays both Zone and Average.')
i_max_line              = input.int(10,                        'Maximum Line Displayed',    minval  = 1,    maxval = 500,                group = 'Inverse Normalized Performance')
i_inv_css               = input.bool(true,                     'Enable Deviation Line Color Switch',                                     group = 'Inverse Normalized Performance', tooltip = 'Switch the line color to differentiate between periods of underperformance and outperformance. Helps in quick visual identification of performance trends.')
i_use_body              = input.bool(false,                    'Use Candle Body',                                                        group = 'Inverse Normalized Performance', tooltip = 'Toggle to decide whether to use the candle body or the wick for determining Outperformance/Underperformance Lines.')
i_line_width            = input.int(1,                         'Line Thickness',            minval  = 1,    maxval = 3,                  group = 'Inverse Normalized Performance', inline  = 'Norminv line style')
i_line_style            = input.string('⎯⎯⎯',                'Line Style',                options = ['⎯⎯⎯', '----', '····'],         group = 'Inverse Normalized Performance', inline  = 'Norminv line style')
i_extend                = input.bool(true,                     'Extend Lines',                                                           group = 'Inverse Normalized Performance')

// ◀─── Utility Variables ───►

// Initialize last_condition and determine price range based on user preference: candle body or full wick
var float last_condition = 0
int       bullcandle     = 0
int       bearcandle     = 0
float     max            = i_use_body ? math.max(open, close) : high
float     min            = i_use_body ? math.min(open, close) : low

// ◀─── Utility Functions ───►

// Function to map string descriptors to line styles
get_line_style(style) =>
    out = switch style
        '⎯⎯⎯' => line.style_solid
        '----'  => line.style_dashed
        '····'  => line.style_dotted

// Function to create lines based on layout preference
createline(value, iswick, isaverage, displaysignal) =>
    if i_sr and displaysignal
        if (i_layout == 'Wick' and iswick) or (i_layout == 'Average' and isaverage) or (i_layout == 'Full Range')
            line.new(bar_index, value, bar_index, value)
        else
            na
    else
        na

// Function to create labels
createlabel(value, isaverage, color, labelprefix) =>
    if i_sr_lb and i_sr
        label.new(bar_index, value, text = labelprefix + ' - ' + str.tostring(math.round_to_mintick(value)), textcolor = color, style = label.style_none)
    else
        na

// ◀─── Inverse Normalized Performance ───►

// Coefficients for the erf function
cof_0            = -1.3026537197817094
cof_1            =  6.4196979235649026e-1
cof_2            =  1.9476473204185836e-2
cof_3            = -9.561514786808631e-3
cof_4            = -9.46595344482036e-4
cof_5            =  3.66839497852761e-4
cof_6            =  4.2523324806907e-5
cof_7            = -2.0278578112534e-5
cof_8            = -1.624290004647e-6
cof_9            =  1.303655835580e-6
cof_10           =  1.5626441722e-8
cof_11           = -8.5238095915e-8
cof_12           =  6.529054439e-9
cof_13           =  5.059343495e-9
cof_14           = -9.91364156e-10
cof_15           = -2.27365122e-10
cof_16           =  9.6467911e-11
cof_17           =  2.394038e-12
cof_18           = -6.886027e-12
cof_19           =  8.94487e-13
cof_20           =  3.13092e-13
cof_21           = -1.12708e-13
cof_22           =  3.81e-16
cof_23           =  7.106e-15
cof_24           = -1.523e-15
cof_25           = -9.4e-17
cof_26           =  1.21e-16
cof_27           = -2.8e-17
p_erf            =  0.3275911

// Coefficients for the erfcinv function
c1               = -0.70711
c2               =  2.30753
c3               =  0.27061
c4               =  0.99229
c5               =  0.04481
constant_erfcinv =  1.12837916709551257

_erf(x) =>
    t  = 1.0 / (1.0 + p_erf * math.abs(x))
    y  = 0.0
    y := y + cof_0  * math.pow(t,      1)
    y := y + cof_1  * math.pow(t,      2)
    y := y + cof_2  * math.pow(t,      3)
    y := y + cof_3  * math.pow(t,      4)
    y := y + cof_4  * math.pow(t,      5)
    y := y + cof_5  * math.pow(t,      6)
    y := y + cof_6  * math.pow(t,      7)
    y := y + cof_7  * math.pow(t,      8)
    y := y + cof_8  * math.pow(t,      9)
    y := y + cof_9  * math.pow(t,     10)
    y := y + cof_10 * math.pow(t,     11)
    y := y + cof_11 * math.pow(t,     12)
    y := y + cof_12 * math.pow(t,     13)
    y := y + cof_13 * math.pow(t,     14)
    y := y + cof_14 * math.pow(t,     15)
    y := y + cof_15 * math.pow(t,     16)
    y := y + cof_16 * math.pow(t,     17)
    y := y + cof_17 * math.pow(t,     18)
    y := y + cof_18 * math.pow(t,     19)
    y := y + cof_19 * math.pow(t,     20)
    y := y + cof_20 * math.pow(t,     21)
    y := y + cof_21 * math.pow(t,     22)
    y := y + cof_22 * math.pow(t,     23)
    y := y + cof_23 * math.pow(t,     24)
    y := y + cof_24 * math.pow(t,     25)
    y := y + cof_25 * math.pow(t,     26)
    y := y + cof_26 * math.pow(t,     27)
    y := y + cof_27 * math.pow(t,     28)
    if x < 0.0
        -y
    else
        y

// Complementary error function
erfc(float x) =>
    1 - _erf(x)

// Inverse complementary error function with improved error handling
erfcinv(float probability) =>
    if (probability <= 0 or                                    probability   >= 2)
        na
    else
        float pp   = (probability  <  1) ? probability  : 2 - probability
        float t    = math.sqrt(-2  * math.log(pp                             / 2))
        float x    = c1 * ((c2 + t * c3) /   (1 + t *         (c4 + t * c5)) - t)

        // First iteration
        float err1 = (1 - _erf(x))                                     - pp
        x         := x  + err1 / (constant_erfcinv  * math.exp(-x * x) - x * err1)

        // Second iteration
        float err2 = (1 - _erf(x))                                    - pp
        x         := x  + err2 / (constant_erfcinv  * math.exp(-x * x) - x * err2)

        (probability < 1) ? x : -x

// Computes the inverse of the standard normal cumulative Underperformance function with specified mean and stdev
norminv(float number, float mean, float stdev) =>
    -1.41421356237309505 * stdev * erfcinv(2 * number) + mean

// Adjust values based on the timeframe
var float mean_value          = 0
var float stddev_value        = 0
var float alpha_value         = 0
var int   value_for_hma       = 0
var float z_threshold_bullish = 0
var float z_threshold_bearish = 0

// Get the current timeframe
current_timeframe             = timeframe.period

// Increment values for each higher timeframe
increment_value               = 0.5
increment_stddev              = 0.1
increment_alpha               = 0.01
increment_hma                 = 1
increment_z_threshold         = 0.1

// Set base values for the smallest timeframe (1 minute)
base_mean_value               = 65.5
base_stddev_value             = 2.5
base_alpha_value              = 0.5
base_hma_value                = 2
base_z_threshold              = 1.5

// Calculate increments based on timeframe
timeframe_multiplier = switch current_timeframe
    '1'   => 0
    '3'   => 1
    '5'   => 2
    '15'  => 3
    '30'  => 4
    '60'  => 5
    '120' => 6
    '240' => 7
    'D'   => 8
    'W'   => 9
    'M'   => 10

// Apply increments to base values
mean_value                   :=  base_mean_value   + increment_value       * timeframe_multiplier
stddev_value                 :=  base_stddev_value + increment_stddev      * timeframe_multiplier
alpha_value                  :=  base_alpha_value  + increment_alpha       * timeframe_multiplier
value_for_hma                :=  base_hma_value    + increment_hma         * timeframe_multiplier
z_threshold_bullish          :=  base_z_threshold  - increment_z_threshold * timeframe_multiplier
z_threshold_bearish          := -base_z_threshold  + increment_z_threshold * timeframe_multiplier

// Calculate norminv values
float bullish_norm_inv        = norminv(alpha_value     / 2, mean_value, stddev_value)
float bearish_norm_inv        = norminv(1 - alpha_value / 2, mean_value, stddev_value)

// Use the adjusted values in the calculation
outperformance                = ta.hma(bullish_norm_inv, value_for_hma) >= z_threshold_bullish and close > open
underperformance              = ta.hma(bearish_norm_inv, value_for_hma) >= z_threshold_bearish and close < open

// Determine whether we should display an label
should_plot_out               = outperformance   and (na(last_condition) or last_condition !=  1)
should_plot_und               = underperformance and (na(last_condition) or last_condition != -1)

// Update the last condition
if      should_plot_out
    last_condition           :=  1
else if should_plot_und
    last_condition           := -1

// Define the timeframes statically
string tf_m1                  =   '1'
string tf_m3                  =   '3'
string tf_m5                  =   '5'
string tf_m15                 =  '15'
string tf_m30                 =  '30'
string tf_h1                  =  '60'
string tf_h2                  = '120'
string tf_h4                  = '240'
string tf_d                   =   'D'
string tf_w                   =   'W'
string tf_m                   =   'M'

// Function to retrieve the outperformance and underperformance signals for a given timeframe
get_signal_from_timeframe(tf) =>
    [plot_out, plot_und] = request.security(syminfo.tickerid, tf, [should_plot_out, should_plot_und])
    [plot_out, plot_und]

// Retrieve signals for each timeframe
[plot_out_m1,  plot_und_m1]  = get_signal_from_timeframe('1')
[plot_out_m3,  plot_und_m3]  = get_signal_from_timeframe('3')
[plot_out_m5,  plot_und_m5]  = get_signal_from_timeframe('5')
[plot_out_m15, plot_und_m15] = get_signal_from_timeframe('15')
[plot_out_m30, plot_und_m30] = get_signal_from_timeframe('30')
[plot_out_h1,  plot_und_h1]  = get_signal_from_timeframe('60')
[plot_out_h2,  plot_und_h2]  = get_signal_from_timeframe('120')
[plot_out_h4,  plot_und_h4]  = get_signal_from_timeframe('240')
[plot_out_d,   plot_und_d]   = get_signal_from_timeframe('D')
[plot_out_w,   plot_und_w]   = get_signal_from_timeframe('W')
[plot_out_m,   plot_und_m]   = get_signal_from_timeframe('M')

// Function to check if signals are concordant across all timeframes
is_signal_concordant() =>
    concordant = true
    if i_multitimeframe_filter
        concordant     := concordant and plot_out_m1  or plot_und_m1
        concordant     := concordant and plot_out_m3  or plot_und_m3
        concordant     := concordant and plot_out_m5  or plot_und_m5
        concordant     := concordant and plot_out_m15 or plot_und_m15
        concordant     := concordant and plot_out_m30 or plot_und_m30
        concordant     := concordant and plot_out_h1  or plot_und_h1
        concordant     := concordant and plot_out_h2  or plot_und_h2
        concordant     := concordant and plot_out_h4  or plot_und_h4
        concordant     := concordant and plot_out_d   or plot_und_d
        concordant     := concordant and plot_out_w   or plot_und_w
        concordant     := concordant and plot_out_m   or plot_und_m
    concordant

// Apply the concordant signal logic
concordant_signal       = is_signal_concordant()

// Check which signal to display based on user's choice
bool display_out_signal = i_multitimeframe_filter ? concordant_signal and should_plot_out : should_plot_out
bool display_und_signal = i_multitimeframe_filter ? concordant_signal and should_plot_und : should_plot_und

// Plot shapes based on inverse normalized performance
plotshape(i_labels and display_out_signal, style = shape.circle, location = location.belowbar, color = color.new(i_out_css, 0), size = size.tiny, title =   'Outperformance')
plotshape(i_labels and display_und_signal, style = shape.circle, location = location.abovebar, color = color.new(i_und_css, 0), size = size.tiny, title = 'Underperformance')

// Alert conditions based on the filtering
alertcondition(display_out_signal, title = 'Outperformance Alert',   message =   'Outperformance detected!')
alertcondition(display_und_signal, title = 'Underperformance Alert', message = 'Underperformance detected!')

// ◀─── Support & Resistance Line ───►

// Initialize variables for handling support and resistance zones, including their lines, labels, tested conditions, and associated colors
var int     numberofline       = i_max_line
var float   upperphzone        = 0
var float   upperplzone        = 0
var float   uppermdzone        = 0
var float   lowerphzone        = 0
var float   lowerplzone        = 0
var float   lowermdzone        = 0
var line    upperphzoneline    = na
var line    upperplzoneline    = na
var line    uppermdzoneline    = na
var line    lowerphzoneline    = na
var line    lowerplzoneline    = na
var line    lowermdzoneline    = na
var label   labelph            = na
var label   labelphmd          = na
var label   labelpl            = na
var label   labelplmd          = na
var bool    upperzonetested    = false
var bool    lowerzonetested    = false
var color   upperzonecolor     = i_und_css
var color   lowerzonecolor     = i_out_css
var line[]  upperphzonearr     = array.new_line(0,    na)
var line[]  upperplzonearr     = array.new_line(0,    na)
var line[]  uppermdzonearr     = array.new_line(0,    na)
var line[]  lowerphzonearr     = array.new_line(0,    na)
var line[]  lowerplzonearr     = array.new_line(0,    na)
var line[]  lowermdzonearr     = array.new_line(0,    na)
var bool[]  upperzonetestedarr = array.new_bool(0, false)
var bool[]  lowerzonetestedarr = array.new_bool(0, false)
var label[] labelpharr         = array.new_label(0,   na)
var label[] labelphmdarr       = array.new_label(0,   na)
var label[] labelplarr         = array.new_label(0,   na)
var label[] labelplmdarr       = array.new_label(0,   na)

// Resistance line calculations and styling
if i_sr and display_und_signal

    // Define the high and low of the resistance zone and calculate the midpoint
    upperphzone     := max
    upperplzone     := min
    uppermdzone     := math.avg(upperphzone, upperplzone)

    // Creating lines and labels for resistance zones
    upperphzoneline := createline(upperphzone, true,  false, display_und_signal)
    upperplzoneline := createline(upperplzone, false, false, display_und_signal)
    uppermdzoneline := createline(uppermdzone, false, true,  display_und_signal)

    // Create a label for the price level at the resistance zone high or midpoint based on user preference
    if i_layout      == 'Wick' or i_layout == 'Full Range'
        labelph   := createlabel(upperphzone, upperphzone, upperzonecolor, 'Underperformance')
    else if i_layout == 'Average'
        labelphmd := createlabel(uppermdzone, uppermdzone, upperzonecolor, 'Underperformance')

    // Remove old lines and labels if they exceed the user-defined maximum number
    if array.size(upperphzonearr) > numberofline
        line.delete(array.shift(upperphzonearr))
        line.delete(array.shift(upperplzonearr))
        line.delete(array.shift(uppermdzonearr))
        array.shift(upperzonetestedarr)
        label.delete(array.shift(labelpharr))
        label.delete(array.shift(labelphmdarr))

    // Store the newly created lines and labels in their respective arrays
    array.push(upperphzonearr,             upperphzoneline)
    array.push(upperplzonearr,             upperplzoneline)
    array.push(uppermdzonearr,             uppermdzoneline)
    array.push(upperzonetestedarr, i_extend ? true : false)
    array.push(labelpharr,                         labelph)
    array.push(labelphmdarr,                     labelphmd)

// If there are any resistance zone lines, update their style and check for crossings
if array.size(upperplzonearr) > 0 or array.size(uppermdzonearr) > 0
    for i = 0 to array.size(upperplzonearr) - 1 by 1
        // Retrieve each line and label from the arrays
        line  tempupperline    = array.get(upperphzonearr,       i)
        line  templowerline    = array.get(upperplzonearr,       i)
        line  tempmiddleline   = array.get(uppermdzonearr,       i)
        label linepricelabel   = array.get(labelpharr,           i)
        label mdlinepricelabel = array.get(labelphmdarr,         i)
        bool  tested           = array.get(upperzonetestedarr,   i)

        // Set the style and width for the lines based on user preferences
        line.set_style(tempupperline,  get_line_style(i_line_style))
        line.set_style(templowerline,  get_line_style(i_line_style))
        line.set_style(tempmiddleline, get_line_style(i_line_style))
        line.set_width(tempupperline,                 i_line_width)
        line.set_width(templowerline,                 i_line_width)
        line.set_width(tempmiddleline,                i_line_width)

        // Set the color for the lines and label based on user preference
        line.set_color(tempupperline,         i_inv_css ? lowerzonecolor : upperzonecolor)
        line.set_color(templowerline,         i_inv_css ? lowerzonecolor : upperzonecolor)
        line.set_color(tempmiddleline,        i_inv_css ? lowerzonecolor : upperzonecolor)
        label.set_textcolor(linepricelabel,   i_inv_css ? lowerzonecolor : upperzonecolor)
        label.set_textcolor(mdlinepricelabel, i_inv_css ? lowerzonecolor : upperzonecolor)

        // Update the label text with the price level
        if i_layout      == 'Wick' or i_layout == 'Full Range'
            label.set_text(linepricelabel, '                                                  Underperformance - ' + str.tostring(math.round_to_mintick(line.get_y1(tempupperline))))
            label.set_size(linepricelabel,   size.small)
            label.set_x(linepricelabel,       bar_index)
        else if i_layout == 'Average'
            label.set_text(mdlinepricelabel, '                                                Underperformance - ' + str.tostring(math.round_to_mintick(line.get_y1(tempmiddleline))))
            label.set_size(mdlinepricelabel, size.small)
            label.set_x(mdlinepricelabel,     bar_index)

        // Check if the current price has crossed the resistance line and update the tested status
        crossed = high > line.get_y1(tempupperline)
        if crossed and not tested
            array.set(upperzonetestedarr, i, true)
            label.delete(linepricelabel)
            label.delete(mdlinepricelabel)

        // Extend the lines to the current bar if necessary based on user settings
        else if i_extend ? tested : not tested
            line.set_x2(tempupperline,        bar_index)
            array.set(upperphzonearr, i,  tempupperline)
            line.set_x2(templowerline,        bar_index)
            array.set(upperplzonearr, i,  templowerline)
            line.set_x2(tempmiddleline,       bar_index)
            array.set(uppermdzonearr, i, tempmiddleline)

// Support line calculations and styling
if i_sr and display_out_signal

    // Calculate the high, low, and average (middle) of the support zones
    lowerphzone     := max
    lowerplzone     := min
    lowermdzone     := math.avg(lowerphzone, lowerplzone)

    // Creating lines and labels for support zones
    lowerphzoneline := createline(lowerphzone, false, false, display_out_signal)
    lowerplzoneline := createline(lowerplzone, true,  false, display_out_signal)
    lowermdzoneline := createline(lowermdzone, false, true,  display_out_signal)

    // Create a label for the price level at the support zone low or midpoint based on user preference
    if i_layout      == 'Wick' or i_layout == 'Full Range'
        labelpl   := createlabel(lowerplzone, lowerplzone, lowerzonecolor, 'Outperformance')
    else if i_layout == 'Average'
        labelplmd := createlabel(lowermdzone, lowermdzone, upperzonecolor, 'Outperformance')

    // Remove old lines and labels if they exceed the user-defined maximum number
    if array.size(lowerphzonearr) > numberofline
        line.delete(array.shift(lowerphzonearr))
        line.delete(array.shift(lowerplzonearr))
        line.delete(array.shift(lowermdzonearr))
        array.shift(lowerzonetestedarr)
        label.delete(array.shift(labelplarr))
        label.delete(array.shift(labelplmdarr))

    // Store the newly created lines and labels in their respective arrays
    array.push(lowerphzonearr,             lowerphzoneline)
    array.push(lowerplzonearr,             lowerplzoneline)
    array.push(lowermdzonearr,             lowermdzoneline)
    array.push(lowerzonetestedarr, i_extend ? true : false)
    array.push(labelplarr,                         labelpl)
    array.push(labelplmdarr,                     labelplmd)

// If there are any support zone lines, update their style and check for crossings
if array.size(lowerplzonearr) > 0
    for i = 0 to array.size(lowerplzonearr) - 1 by 1

        // Retrieve each line and label from the arrays
        line  tempupperline  = array.get(lowerphzonearr,         i)
        line  templowerline  = array.get(lowerplzonearr,         i)
        line  tempmiddleline = array.get(lowermdzonearr,         i)
        label linepricelabel = array.get(labelplarr,             i)
        label mdlinepricelabel = array.get(labelplmdarr,         i)
        bool  tested         = array.get(lowerzonetestedarr,     i)

        // Set the style and width for the lines based on user preferences
        line.set_style(tempupperline,  get_line_style(i_line_style))
        line.set_style(templowerline,  get_line_style(i_line_style))
        line.set_style(tempmiddleline, get_line_style(i_line_style))
        line.set_width(tempupperline,                 i_line_width)
        line.set_width(templowerline,                 i_line_width)
        line.set_width(tempmiddleline,                i_line_width)

        // Set the color for the lines and label based on user preference
        line.set_color(tempupperline,         i_inv_css ? upperzonecolor : lowerzonecolor)
        line.set_color(templowerline,         i_inv_css ? upperzonecolor : lowerzonecolor)
        line.set_color(tempmiddleline,        i_inv_css ? upperzonecolor : lowerzonecolor)
        label.set_textcolor(linepricelabel,   i_inv_css ? upperzonecolor : lowerzonecolor)
        label.set_textcolor(mdlinepricelabel, i_inv_css ? upperzonecolor : lowerzonecolor)

        // Update the label text with the price level
        if i_layout      == 'Wick' or i_layout == 'Full Range'
            label.set_text(linepricelabel, '                                              Outperformance - ' + str.tostring(math.round_to_mintick(line.get_y1(templowerline))))
            label.set_size(linepricelabel,   size.small)
            label.set_x(linepricelabel,       bar_index)
        else if i_layout == 'Average'
            label.set_text(mdlinepricelabel, '                                            Outperformance - ' + str.tostring(math.round_to_mintick(line.get_y1(tempmiddleline))))
            label.set_size(mdlinepricelabel, size.small)
            label.set_x(mdlinepricelabel,     bar_index)

        // Check if the current price has crossed the support line and update the tested status
        crossed = low < line.get_y1(templowerline)
        if crossed and not tested
            array.set(lowerzonetestedarr, i, true)
            label.delete(linepricelabel)
            label.delete(mdlinepricelabel)

        // Extend the lines to the current bar if necessary based on user settings
        else if i_extend ? tested : not tested
            line.set_x2(tempupperline,        bar_index)
            array.set(lowerphzonearr, i,  tempupperline)
            line.set_x2(templowerline,        bar_index)
            array.set(lowerplzonearr, i,  templowerline)
            line.set_x2(tempmiddleline,       bar_index)
            array.set(lowermdzonearr, i, tempmiddleline)

// ◀─── Deviation Bands ───►

// Function to calculate the inverse normal distribution based on the dynamic alpha value
norminv_dynamic(alpha_value) =>
    local_mean_value       = ta.hma(close,                                       value_for_hma)
    local_stddev_value     = ta.stdev(close,                                     value_for_hma)
    local_bullish_norm_inv = norminv(alpha_value     / 2, local_mean_value, local_stddev_value)
    local_bearish_norm_inv = norminv(1 - alpha_value / 2, local_mean_value, local_stddev_value)
    [local_bullish_norm_inv, local_bearish_norm_inv]

// Retrieve the inverse normal values for the current dynamic alpha
[local_bullish_inv, local_bearish_inv] = norminv_dynamic(alpha_value)

// Calculate the dynamic deviation bands based on the local inverse normal values
upper_band_value  = local_bullish_inv
lower_band_value  = local_bearish_inv
middle_band_value = math.avg(upper_band_value, lower_band_value)

// Plotting the dynamic deviation bands
upper_band        = plot(i_bands ? upper_band_value  : na, color = i_und_css,      title =  'Upper Band')
middle_band       = plot(i_bands ? middle_band_value : na, color = chart.fg_color, title = 'Middle Band')
lower_band        = plot(i_bands ? lower_band_value  : na, color = i_out_css,      title =  'Lower Band')

// Fill the space between the upper and lower bands with a shade
fill(upper_band, lower_band, color = color.rgb(173, 216, 230, 90), title = 'Band Shade')

// Alert conditions for crossing the bands
alertcondition(ta.crossover(high,   upper_band_value),                                            title = 'Price Crossed Above Upper Band', message = 'The price has closed above the upper deviation band, indicating potential overbought conditions.')
alertcondition(ta.crossunder(low,   lower_band_value),                                            title = 'Price Crossed Below Lower Band', message = 'The price has closed below the lower deviation band, indicating potential oversold conditions.')
alertcondition(ta.crossover(close, middle_band_value) or ta.crossunder(close, middle_band_value), title = 'Price Crossed Middle Band',      message = 'The price has crossed the middle deviation band, suggesting a possible change in the price trend.')

// Conditions for touching the bands
alertcondition(high  >= upper_band_value,                                                         title = 'Price Touched Upper Band',       message = 'The price has reached or exceeded the upper deviation band, which may signal upper resistance.')
alertcondition(low   <= lower_band_value,                                                         title = 'Price Touched Lower Band',       message = 'The price has reached or dropped below the lower deviation band, which may indicate lower support.')
alertcondition(close == middle_band_value,                                                        title = 'Price Touched Middle Band',      message = 'The price has touched the middle deviation band, potentially signaling a pause or hesitation in the trend.')