AI Moving Average (Expo)
// 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/

//@version=5
indicator("AI Moving Average (Expo)", overlay=true)

// ~~ ToolTips {
t1 ="Number of neighbors used in the KNN algorithm. Increasing 'k' can make the prediction more resilient to noise but may decrease sensitivity to local variations.\n\nNumber of data points considered in the AI analysis. This affects how the AI interprets patterns in the price data."
t2 ="Type of moving average to be applied. Various options allow for different smoothing techniques which can emphasize or dampen certain aspects of the price movement. \n\nLength of the moving average. A greater length will create a smoother curve but might lag behind recent price changes."
t3 ="Source data used to classify price as bullish or bearish. Can be adjusted to consider different aspects of the price information."
t4 ="Source data used to calculate the moving average for comparison. Different selections may emphasize different aspects of price movement."
t5 ="Toggle to show or hide the AI-generated moving average line."
t6 ="Toggle to show or hide the AI-calculated slope of the trend."
t7 ="Option to remove or retain historical boxes."
t8 ="Smoothing period for the initial moving average in the AI(2) algorithm."
t9 ="Toggle to remove all boxes from the chart."
t10 ="Toggle to remove all lines from the chart."
t11 = "Number of closest values used for the calculation of AI(2) Moving Averages. A higher number may provide a smoother but less sensitive curve."
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

//
aiMa       = input.bool(false, title="Show AI(1) Moving Average", group="AI(1) Average", tooltip=t5)
aiSlope    = input.bool(true, title="Show AI(1) Slope", group="AI(1) Average", tooltip=t6)

maType = input.string("SMA", title="Select MA", options=["SMA", "EMA", "WMA", "RMA", "HMA", "VWMA"], inline="MA", group="AI(1) Average")
length = input.int(50, title="", minval=1, inline="MA", group="AI(1) Average",tooltip=t2)

// ~~ Input settings for K and N values
k  = input.int(5, title="Neighbors", minval=1, maxval=100, inline="AI", group="AI(1) Settings") 
n_ = input.int(10, title="DataPoints", minval=1, maxval=100, inline="AI", group="AI(1) Settings",tooltip=t1)
n  = math.max(k, n_)

aiMa_2  = input.bool(false, title="Show AI(2) Moving Average 1", group="AI(2) Average", tooltip=t5)
aiMa_3  = input.bool(true, title="Show AI(2) Moving Average 2", group="AI(2) Average", tooltip=t6)

// Input parameters for the KNN Moving Average
numberOfClosestValues = input.int(15, "Number of Closest Values", 2, 200, inline="AI2", group="AI(2) Settings", tooltip=t11)
smoothingPeriod       = input.int(20, "Smoothing Period", 2, 500, inline="aiMa_2", group="AI(2) Settings", tooltip=t8)
windowSize            = math.max(numberOfClosestValues, 30) 

dataToClassify        = input.source(close, title= "Data To Classify", group="AI(1) Settings",tooltip=t3)
dataForMovingAverage  = input.source(close, title= "Data For Moving Average (DataForComparison)", group="AI(1) Settings",tooltip=t4)
negBox_col = input.color(color.new(color.red,75),title="", group="Style", inline="box")
posBox_col = input.color(color.new(color.lime,75),title="", group="Style", inline="box")
line_col   = input.color(color.rgb(255, 169, 39),title="", group="Style", inline="box")
line_width = input.int(2, title="",group="Style", inline="box", tooltip="Box & Line coloring")
del        = input.bool(false, title="Remove Historical Boxes", group="Style", tooltip=t7)
del_boxes  = input.bool(true, title="Remove All Boxes", group="Style", tooltip=t9)
del_line   = input.bool(true,title="Remove All Line", group="Style", tooltip=t10)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Calculate the selected type of moving average based on the user's choice
ma = switch maType
    "SMA"  => ta.sma(dataForMovingAverage, length)
    "EMA"  => ta.ema(dataForMovingAverage, length)
    "WMA"  => ta.wma(dataForMovingAverage, length)
    "RMA"  => ta.rma(dataForMovingAverage, length)
    "HMA"  => ta.hma(dataForMovingAverage, length)
    "VWMA" => ta.vwma(dataForMovingAverage,length)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Collect data points and their corresponding labels
labels = array.new_float(n) 
data   = array.new_float(n) 

for i = 0 to n - 1
    data.set(i, ma[i]) 
    label_i = dataToClassify[i] > ma[i] ? 1 : 0 
    label_j = ma[i] > ma[i+1] ? 1 : 0 
    lab     = int(math.avg(label_i,label_j))
    labels.set(i,lab) 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Define the weighted k-nearest neighbors (KNN) function
knn_weighted(data, labels, k, x) =>
    n1 = data.size() 
    distances = array.new_float(n1) 
    indices = array.new_int(n1) 
    // Compute distances from the current point to all other points
    for i = 0 to n1 - 1 
        x_i = data.get(i)
        dist = math.abs(x-x_i)
        distances.set(i, dist)
        indices.set(i, i)
    // Sort distances and corresponding indices in ascending order
    // Bubble sort method
    for i = 0 to n1 - 2
        for j = 0 to n1 - i - 2
            if distances.get(j) > distances.get(j + 1)
                tempDist = distances.get(j) 
                distances.set(j, distances.get(j + 1))
                distances.set(j + 1, tempDist)
                tempIndex = indices.get(j)
                indices.set(j, indices.get(j + 1))
                indices.set(j + 1, tempIndex)
    // Compute weighted sum of labels of the k nearest neighbors
    weighted_sum = 0.
    total_weight = 0.
    for i = 0 to k - 1
        index = indices.get(i)
        label_i = labels.get(index)
        weight_i = 1 / (distances.get(i) + 1e-6) 
        weighted_sum += weight_i * label_i
        total_weight += weight_i
    weighted_sum / total_weight 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Classify the current data point
label_      = knn_weighted(data, labels, k, ma) 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ kNN Classifier {
meanOfKClosest(value_in_,target_) => 
    closestDistances = array.new_float(numberOfClosestValues, 1e10) 
    closestValues = array.new_float(numberOfClosestValues, 0.0) 
    for i = 1 to windowSize 
        value = value_in_[i] 
        distance = math.abs(target_ - value) 
        maxDistIndex = 0 
        maxDistValue = closestDistances.get(0) 
        for j = 1 to numberOfClosestValues - 1 
            if closestDistances.get(j) > maxDistValue
                maxDistIndex := j
                maxDistValue := closestDistances.get(j)
        if distance < maxDistValue 
            closestDistances.set(maxDistIndex, distance)
            closestValues.set(maxDistIndex, value)
    closestValues.sum() / numberOfClosestValues 
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// Initializes the KNN Moving Average.
ma_            = ta.sma(close,smoothingPeriod)
knnMA_price_ma = meanOfKClosest(close,ma_)
KnnMA_ma_ma    = meanOfKClosest(ma_,ma_)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Box Conditions
red        = label_ == 0
green      = label_ == 1
blue       = not red and not green
prevRed    = red[1]
prevGreen  = green[1]
prevBlue   = blue[1]
switchdown = (prevBlue or prevGreen) and red
switchup   = (prevBlue or prevRed) and green
redtoblue  = prevRed and blue
greentoblue= prevGreen and blue
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Slope
// ~~ Declare variables to hold the values for x and y
var float[] x_values = array.new_float(0)
var float[] y_values = array.new_float(0)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Clear x and y values when switching trends
if switchdown or switchup
    x_values.clear()
    y_values.clear()
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Collect x and y values
x_values.push(bar_index)
y_values.push(ma)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Perform linear regression if enough values are collected
float slope = na
float intercept = na
if x_values.size() > 1
    float sum_x = x_values.sum()
    float sum_y = y_values.sum()
    float sum_xy = 0.
    float sum_x2 = 0.
    int count = x_values.size()
    for int i = 0 to count - 1
        float x = x_values.get(i)
        float y = y_values.get(i)
        sum_xy := sum_xy + x * y
        sum_x2 := sum_x2 + x * x
    slope := (count * sum_xy - sum_x * sum_y) / (count * sum_x2 - sum_x * sum_x)
    intercept := (sum_y - slope * sum_x) / count

line_eq = na(slope) ? na : slope * bar_index + intercept
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Plot Box
var bo = box(na)
var li = line(na)
var maxHi = float(na)
var minLo = float(na)
highPoint = ta.highest(high,20)
lowPoint  = ta.lowest(low,20)
if switchdown or switchup
    if del
        bo.delete()
    maxHi := na(highPoint) ? high : math.max(highPoint, high)
    minLo := na(lowPoint) ? low : math.min(lowPoint, low)
    bo := box.new(bar_index,maxHi,bar_index+1,minLo,na,
     bgcolor=label_>0?posBox_col:negBox_col)
    mid = chart.point.now(math.avg(maxHi,minLo))
    li := line.new(mid,mid,color=line_col,style=line.style_dotted,width=line_width)
    
maxHi := math.max(maxHi,high)
minLo := math.min(minLo,low)
bo.set_top(maxHi)
bo.set_rightbottom(bar_index,minLo)
li.set_y1(math.avg(maxHi,minLo))
li.set_xy2(bar_index,math.avg(maxHi,minLo))
if del_boxes
    bo.delete()
if del_line
    li.delete()
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}

// ~~ Plots
plot(aiMa?ma:na, title="AI(1) Bullish Trend",color=label_ == 1 ? color.lime : na, linewidth = 2)
plot(aiMa?ma:na, title="AI(1) Bearish Trend",color=label_ == 0 ? color.red : na, linewidth = 2)
plot(aiMa?ma:na, title="AI(1) Moving Average",color=label_ == 1 ? na : label_ == 0 ? na: color.rgb(0, 136, 255))
plot(aiSlope?line_eq:na,title="AI(1) Box Slope", color=slope > 0 ? color.lime : color.red, linewidth=1)
plot(aiMa_2?ta.wma(knnMA_price_ma,5):na, color=color.rgb(31, 199, 255), title="AI(2) Moving Average 1")
plot(aiMa_3?ta.wma(KnnMA_ma_ma,5):na, color=color.yellow, title="AI(2) Moving Average 2")
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}