//@version=5
indicator(title="RSI & Composite Trend Oscillator", shorttitle="RSI+CTO", overlay=false, precision=2)

// -------------------------------
// Section 1: Relative Strength Index (RSI)
// -------------------------------

// Function to calculate Moving Averages with unique naming to avoid conflicts
ma_RSI(source, length, type) =>
    switch type
        "SMA" => ta.sma(source, length)
        "Bollinger Bands" => ta.sma(source, length)
        "EMA" => ta.ema(source, length)
        "SMMA (RMA)" => ta.rma(source, length)
        "WMA" => ta.wma(source, length)
        "VWMA" => ta.vwma(source, length)

// RSI Inputs
rsiLength_RSI = input.int(14, minval=1, title="RSI Length", group="RSI Settings")
rsiSource_RSI = input.source(close, "Source", group="RSI Settings")
maType_RSI = input.string("SMA", title="MA Type", 
    options=["SMA", "Bollinger Bands", "EMA", "SMMA (RMA)", "WMA", "VWMA"], 
    group="MA Settings")
maLength_RSI = input.int(14, title="MA Length", group="MA Settings")
bbMult_RSI = input.float(2.0, minval=0.001, maxval=50, title="BB StdDev", group="MA Settings")
showDivergence_RSI = input.bool(false, title="Show Divergence", group="RSI Settings")

// Calculate RSI and its Moving Average
up_RSI = ta.rma(math.max(ta.change(rsiSource_RSI), 0), rsiLength_RSI)
down_RSI = ta.rma(-math.min(ta.change(rsiSource_RSI), 0), rsiLength_RSI)
rsi_RSI = down_RSI == 0 ? 100 : (up_RSI == 0 ? 0 : 100 - (100 / (1 + up_RSI / down_RSI)))
rsiMA_RSI = ma_RSI(rsi_RSI, maLength_RSI, maType_RSI)
isBB_RSI = (maType_RSI == "Bollinger Bands")

// Plot RSI and its Moving Average
plot(rsi_RSI, title="RSI", color=color.new(#7E57C2, 0))
plot(rsiMA_RSI, title="RSI-based MA", color=color.yellow)
rsiUpperBand_RSI = hline(70, "RSI Upper Band", color=#787B86)
midline_RSI = hline(50, "RSI Middle Band", color=color.new(#787B86, 50))
rsiLowerBand_RSI = hline(30, "RSI Lower Band", color=#787B86)
fill(rsiUpperBand_RSI, rsiLowerBand_RSI, color=color.rgb(126, 87, 194, 90), title="RSI Background Fill")
bbUpperBand_RSI = plot(isBB_RSI ? rsiMA_RSI + ta.stdev(rsi_RSI, maLength_RSI) * bbMult_RSI : na, title="Upper Bollinger Band", color=color.green)
bbLowerBand_RSI = plot(isBB_RSI ? rsiMA_RSI - ta.stdev(rsi_RSI, maLength_RSI) * bbMult_RSI : na, title="Lower Bollinger Band", color=color.green)
fill(bbUpperBand_RSI, bbLowerBand_RSI, color=isBB_RSI ? color.new(color.green, 90) : na, title="Bollinger Bands Background Fill")

midLinePlot_RSI = plot(50, title="Mid Line", color=na, editable=false, display=display.none)
fill(rsi_RSI, midLinePlot_RSI, color=color.new(color.green, 0), title="Overbought Gradient Fill")
fill(rsi_RSI, midLinePlot_RSI, color=color.new(color.red, 0), title="Oversold Gradient Fill")

// Divergence Settings
lookbackRight_RSI = 5
lookbackLeft_RSI = 5
rangeUpper_RSI = 60
rangeLower_RSI = 5
bearColor_RSI = color.red
bullColor_RSI = color.green
textColor_RSI = color.white
noneColor_RSI = color.new(color.white, 100)

// Detect Pivot Points
plFound_RSI = not na(ta.pivotlow(rsi_RSI, lookbackLeft_RSI, lookbackRight_RSI))
phFound_RSI = not na(ta.pivothigh(rsi_RSI, lookbackLeft_RSI, lookbackRight_RSI))

// Function to check if bars since condition are within range
_inRange_RSI(cond) =>
    bars = ta.barssince(cond)
    rangeLower_RSI <= bars and bars <= rangeUpper_RSI

// Regular Bullish Divergence
rsiHL_RSI = (rsi_RSI[lookbackRight_RSI] > ta.valuewhen(plFound_RSI, rsi_RSI[lookbackRight_RSI], 1)) and _inRange_RSI(plFound_RSI[1])
priceLL_RSI = low[lookbackRight_RSI] < ta.valuewhen(plFound_RSI, low[lookbackRight_RSI], 1)
bullCondAlert_RSI = priceLL_RSI and rsiHL_RSI and plFound_RSI
bullCond_RSI = showDivergence_RSI and bullCondAlert_RSI

plot(plFound_RSI ? rsi_RSI[lookbackRight_RSI] : na,
     offset=-lookbackRight_RSI,
     title="Regular Bullish",
     linewidth=2,
     color=(bullCond_RSI ? bullColor_RSI : noneColor_RSI),
     display=display.pane)

plotshape(bullCond_RSI ? rsi_RSI[lookbackRight_RSI] : na,
         offset=-lookbackRight_RSI,
         title="Regular Bullish Label",
         text=" Bull ",
         style=shape.labelup,
         location=location.absolute,
         color=bullColor_RSI,
         textcolor=textColor_RSI)

// Regular Bearish Divergence
rsiLH_RSI = (rsi_RSI[lookbackRight_RSI] < ta.valuewhen(phFound_RSI, rsi_RSI[lookbackRight_RSI], 1)) and _inRange_RSI(phFound_RSI[1])
priceHH_RSI = high[lookbackRight_RSI] > ta.valuewhen(phFound_RSI, high[lookbackRight_RSI], 1)
bearCondAlert_RSI = priceHH_RSI and rsiLH_RSI and phFound_RSI
bearCond_RSI = showDivergence_RSI and bearCondAlert_RSI

plot(phFound_RSI ? rsi_RSI[lookbackRight_RSI] : na,
     offset=-lookbackRight_RSI,
     title="Regular Bearish",
     linewidth=2,
     color=(bearCond_RSI ? bearColor_RSI : noneColor_RSI),
     display=display.pane)

plotshape(bearCond_RSI ? rsi_RSI[lookbackRight_RSI] : na,
         offset=-lookbackRight_RSI,
         title="Regular Bearish Label",
         text=" Bear ",
         style=shape.labeldown,
         location=location.absolute,
         color=bearColor_RSI,
         textcolor=textColor_RSI)

// Alert Conditions
alertcondition(bullCondAlert_RSI, title='Regular Bullish Divergence', 
    message="Found a new Regular Bullish Divergence, `Pivot Lookback Right` number of bars to the left of the current bar.")
alertcondition(bearCondAlert_RSI, title='Regular Bearish Divergence', 
    message='Found a new Regular Bearish Divergence, `Pivot Lookback Right` number of bars to the left of the current bar.')

// -------------------------------
// Section 2: Composite Trend Oscillator (CTO)
// -------------------------------

// Functions for CTO to avoid naming conflicts
PhiSmoother_CTO(source, length, phase=3.7) =>
    var array<float> coefs = na
    var int length_1 = length - 1
    var float W = 0.0
    if na(coefs)
        coefs := array.new_float()
        const float SQRT_PIx2_CTO = math.sqrt(2.0 * math.pi)
        const float MULTIPLIER_CTO = -0.5 / 0.93
        var float length_2 = length * 0.52353
        for i = 0 to length_1
            float alpha = (i + phase - length_2) * MULTIPLIER_CTO
            float beta = 1.0 / (0.2316419 * math.abs(alpha) + 1.0)
            // Break down phi calculation into intermediate steps to prevent line breaks
            float phi_part1 = math.exp(math.pow(alpha, 2) * -0.5) * -0.398942280
            float phi_part2 = 0.319381530 + beta * (-0.356563782 + beta * (1.781477937 + beta * (-1.821255978 + beta * 1.330274429)))
            float phi_intermediate = (phi_part1 * beta * phi_part2)
            float phi = phi_intermediate + 1.011
            if alpha < 0.0
                phi := 1.0 - phi
            float weight = phi / SQRT_PIx2_CTO
            array.push(coefs, weight)
            W += weight
    float sma2 = ta.sma(source, 2)
    float E = 0.0
    for i = 0 to length_1
        E += array.get(coefs, i) * sma2[i]
    E / W

ema_CTO(source, length) =>
    ta.ema(source, length)

dema_CTO(source, length) =>
    ema1 = ta.ema(source, length)
    ema2 = ta.ema(ema1, length)
    2 * ema1 - ema2

tema_CTO(source, length) =>
    ema1 = ta.ema(source, length)
    ema2 = ta.ema(ema1, length)
    ema3 = ta.ema(ema2, length)
    (ema1 - ema2) * 3 + ema3

wma_CTO(source, length) =>
    ta.wma(source, length)

sma_CTO(source, length) =>
    ta.sma(source, length)

filter_CTO(source, length, phase, style) =>
    if length > 1
        switch style
            "PhiSmoother" => PhiSmoother_CTO(source, length, phase)
            "EMA" => ta.ema(source, length)
            "DEMA" => dema_CTO(source, length)
            "TEMA" => tema_CTO(source, length)
            "WMA" => wma_CTO(source, length)
            => ta.sma(source, length) // Default to "SMA"
    else
        source

// Scoring Functions
get_score_CTO(source_array) =>
    array<float> scores = array.new_float()
    for i = 0 to array.size(source_array) - 1
        float current = array.get(source_array, i)
        int score_sum = 0
        for j = 0 to array.size(source_array) - 1
            if i != j
                float check = array.get(source_array, j)
                int polarity = i < j ? 1 : -1
                if current > check
                    score_sum += polarity
                else
                    score_sum -= polarity
        array.push(scores, score_sum)
    scores

net_score_CTO(scores_array) =>
    int value = array.size(scores_array) - 1
    // Calculate average of scores
    float avg_score = 0.0
    for i = 0 to array.size(scores_array) - 1
        avg_score += array.get(scores_array, i)
    avg_score /= array.size(scores_array)
    float netScore = ((avg_score + value) / (value * 2.0) - 0.5) * 200.0
    netScore

// Color Calculation Function
get_color_CTO(score, transition_easing, volatility_mode, rising_bullish, falling_bullish, falling_bearish, rising_bearish, rising_transition, falling_transition) =>
    var color grad = na
    float delta = score - nz(score[1], score)
    color bullish = delta >= 0.0 ? rising_bullish : falling_bullish
    color bearish = delta >  0.0 ? rising_bearish : falling_bearish
    color transit = score > 0.0 ? (delta >= 0.0 ? rising_transition : falling_transition) : (delta >= 0.0 ? falling_transition : rising_transition)
    if volatility_mode
        float easing = 0.01 * transition_easing
        float crms = easing * math.sqrt(ta.cum(math.pow(math.abs(score), 2)) / (bar_index + 1))
        grad := score > 0.0 ?
                (transition_easing > 0.0 ? color.from_gradient(score,  0.0, crms, transit, bullish) : bullish) :
                (transition_easing > 0.0 ? color.from_gradient(score, -crms, 0.0, bearish, transit) : bearish)
    else
        grad := score > 0.0 ?
                (transition_easing > 0.0 ? color.from_gradient(score,  0.0, transition_easing, transit, bullish) : bullish) :
                (transition_easing > 0.0 ? color.from_gradient(score, -transition_easing, 0.0, bearish, transit) : bearish)
    grad

// CTO Inputs
common_CTO = "Common Controls"

// Main Inputs
source_CTO = input.source(close, "Source", group=common_CTO)
mode_CTO = input.string("Trend Strength", "Composite Cluster Mode", group=common_CTO, 
    options=["Trend Strength", "Volatility"], 
    tooltip="Trend Strength visualizes the directionality of the filter cluster. Volatility weights the score to the bandwidth of the cluster.")
filter_CTO = input.string("PhiSmoother", "Cluster Filter", group=common_CTO, 
    options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"], 
    tooltip="Choose a filter to build the moving average cluster with.")
phase_CTO = input.float(3.7, "PhiSmoother Phase", group=common_CTO, minval=0.0, step=0.1, 
    tooltip="Adjust the phase/lag for PhiSmoother.")

// Cluster Options
cluster_CTO = "Cluster Options"
spacing_CTO = input.int(3, "Cluster Dispersion", group=cluster_CTO, minval=1, maxval=10, 
    tooltip="Separation between the moving averages in the cluster.")
upper_trim_CTO = input.int(0, "Cluster Trim - Upper:", group=cluster_CTO, inline="trim", minval=0, maxval=31)
lower_trim_CTO = input.int(0, " Lower:", group=cluster_CTO, inline="trim", minval=0, maxval=31, 
    tooltip="The 'Upper' parameter modifies the shortest period of the moving averages, whereas 'Lower' parameter adjusts the longest period. Increasing the upper value reduces sensitivity, while increasing the lower value heightens sensitivity.")

// Post Smoothing Settings
output_CTO = "Composite Post Smoothing"
post_smooth_filt_CTO = input.string("PhiSmoother", "PostSmooth - Filter:", group=output_CTO, 
    inline="post", options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"])
post_smooth_len_CTO = input.int(1, " Length:", group=output_CTO, inline="post", minval=1, 
    tooltip="Period for the cluster's post smoothing.")

// Composite Signal Settings
signal_CTO = "Composite Signal Settings"
signal_filter_CTO = input.string("PhiSmoother", "Signal - Filter:", group=signal_CTO, 
    inline="signal", options=["PhiSmoother", "EMA", "DEMA", "TEMA", "WMA", "SMA"])
signal_length_CTO = input.int(20, " Length:", group=signal_CTO, inline="signal", minval=1, 
    tooltip="Period for the momentum signal plot.")
signal_color_CTO = input.color(color.new(color.blue, 0), "Filter Color", group=signal_CTO)

// Threshold Levels
threshold_CTO = "Threshold Levels"
upperLevel_CTO = input.float(75.00, "Levels - Upper:", group=threshold_CTO, inline="level", 
    minval=1.0, maxval=99.0, step=2.0)
lowerLevel_CTO = input.float(-75.00, " Lower:", group=threshold_CTO, inline="level", 
    minval=-99.0, maxval=-1.0, step=2.0, 
    tooltip="Adjust the thresholds as needed.")

// Coloring Preferences
colors_CTO = "Coloring Preferences"
candle_color_CTO = input.bool(false, "Candle Coloring", group=colors_CTO)
transition_easing_CTO = input.float(50.0, "Transition Easing", group=colors_CTO, 
    maxval=100.0, minval=0.0, step=5.0, 
    tooltip="Adjust sensitivity to ranging conditions.")
fill_bg_CTO = input.bool(true, "Fill Background", group=colors_CTO, inline="fill")
fill_alpha_CTO = input.int(85, "", group=colors_CTO, inline="fill", minval=0, maxval=100)
rising_bullish_CTO = input.color(#FFCC00, "Bullish Color", group=colors_CTO, inline="bull")
falling_bullish_CTO = input.color(#FDf0B9, "", group=colors_CTO, inline="bull")
rising_transition_CTO = input.color(#9598A1, "Transition Color", group=colors_CTO, inline="range")
falling_transition_CTO = input.color(#CCCCCC, "", group=colors_CTO, inline="range")
falling_bearish_CTO = input.color(#5500CC, "Bearish Color", group=colors_CTO, inline="bear")
rising_bearish_CTO = input.color(#B580FF, "", group=colors_CTO, inline="bear")

VOLATILITY_MODE_ON_CTO = (mode_CTO == "Volatility")

// Create Cluster
array<float> filter_cluster_CTO = array.new_float()
array.push(filter_cluster_CTO, source_CTO)
for i = 1 to 33
    length_i = i * spacing_CTO
    filtered = switch filter_CTO
        "PhiSmoother" => PhiSmoother_CTO(source_CTO, length_i, phase_CTO)
        "EMA" => ta.ema(source_CTO, length_i)
        "DEMA" => dema_CTO(source_CTO, length_i)
        "TEMA" => tema_CTO(source_CTO, length_i)
        "WMA" => ta.wma(source_CTO, length_i)
        => ta.sma(source_CTO, length_i) // Default to "SMA"
    array.push(filter_cluster_CTO, filtered)

// Trim Clusters
if upper_trim_CTO > 0
    for i = 0 to math.min(upper_trim_CTO - 1, array.size(filter_cluster_CTO) - 1)
        if array.size(filter_cluster_CTO) > 2
            array.shift(filter_cluster_CTO)

if lower_trim_CTO > 0
    for i = 0 to math.min(lower_trim_CTO - 1, array.size(filter_cluster_CTO) - 1)
        if array.size(filter_cluster_CTO) > 2
            array.pop(filter_cluster_CTO)

// Calculate Ribbon Metrics
float ribbon_max_CTO = array.max(filter_cluster_CTO)
float ribbon_min_CTO = array.min(filter_cluster_CTO)
float ribbon_width_CTO = ribbon_max_CTO - ribbon_min_CTO
float ribbon_rank_CTO = VOLATILITY_MODE_ON_CTO ? nz(ribbon_width_CTO / math.avg(ribbon_max_CTO, ribbon_min_CTO)) : 1

// Calculate Score and Net Score
array<float> score_CTO = get_score_CTO(filter_cluster_CTO)
float net_score_raw_CTO = net_score_CTO(score_CTO)

// Apply Post Smoothing
float net_score_CTO_val = switch filter_CTO
    "PhiSmoother" => PhiSmoother_CTO(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO, 3.7)
    "EMA" => ta.ema(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO)
    "DEMA" => dema_CTO(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO)
    "TEMA" => tema_CTO(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO)
    "WMA" => ta.wma(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO)
    => ta.sma(net_score_raw_CTO * ribbon_rank_CTO, post_smooth_len_CTO) // Default to "SMA"

// Calculate Colors
color main_color_CTO = get_color_CTO(net_score_CTO_val, transition_easing_CTO, VOLATILITY_MODE_ON_CTO, 
    rising_bullish_CTO, falling_bullish_CTO, falling_bearish_CTO, rising_bearish_CTO, 
    rising_transition_CTO, falling_transition_CTO)

// Calculate Signal
float signal_value_CTO = signal_length_CTO < 2 ? na :
    switch signal_filter_CTO
        "PhiSmoother" => PhiSmoother_CTO(ta.sma(net_score_CTO_val, 2), signal_length_CTO, 3.7)
        "EMA" => ta.ema(ta.sma(net_score_CTO_val, 2), signal_length_CTO)
        "DEMA" => dema_CTO(ta.sma(net_score_CTO_val, 2), signal_length_CTO)
        "TEMA" => tema_CTO(ta.sma(net_score_CTO_val, 2), signal_length_CTO)
        "WMA" => ta.wma(ta.sma(net_score_CTO_val, 2), signal_length_CTO)
        => ta.sma(net_score_CTO_val, 2) // Default to "SMA"

// Plot Threshold Lines
top_CTO = hline(VOLATILITY_MODE_ON_CTO ? na : 100.0, "Top", color=#FF0000)
upper_CTO = hline(VOLATILITY_MODE_ON_CTO ? na : upperLevel_CTO, "+Level", rising_bullish_CTO, hline.style_dotted, 2)
center_CTO = hline(0.0, "Center", color=#CCCCCC)
lower_CTO = hline(VOLATILITY_MODE_ON_CTO ? na : lowerLevel_CTO, "+Level", falling_bearish_CTO, hline.style_dotted, 2)
bottom_CTO = hline(VOLATILITY_MODE_ON_CTO ? na : -100.0, "Bottom", color=#00FF00)

// Fill Areas Between Threshold Lines
const color invisible_CTO = color.new(color.black, 0)
fill(top_CTO, upper_CTO, color=color.new(color.maroon, 0), title="Top to Upper Fill")
fill(center_CTO, upper_CTO, color=color.new(rising_bullish_CTO, 100), title="Center to Upper Fill")
fill(center_CTO, lower_CTO, color=color.new(falling_bearish_CTO, 100), title="Center to Lower Fill")
fill(bottom_CTO, lower_CTO, color=color.new(color.green, 0), title="Bottom to Lower Fill")

// Candle Coloring
barcolor(candle_color_CTO ? main_color_CTO : na)

// Plot Score and Signal
plot(net_score_CTO_val, title="Score", color=main_color_CTO, linewidth=3)
plot(0.0, title="Zero Line", color=invisible_CTO)
plot(signal_value_CTO, title="Signal", color=signal_color_CTO, linewidth=1)

// Fill Below Score Plot
fill(
    net_score_CTO_val > 0.0 ? net_score_CTO_val : na,
    0.0,
    color=net_score_CTO_val > 0.0 ? color.new(rising_bullish_CTO, fill_bg_CTO and VOLATILITY_MODE_ON_CTO ? fill_alpha_CTO : 100) : 
                                 color.new(falling_bearish_CTO, 100),
    title="Score Fill"
)