Machine Learning: Trend Lines [YinYangAlgorithms]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    ,@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@      @@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@        @@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@         @@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@           @@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@        .@@@@@@@@@@@@@@@            @@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@          *@@@@@@@@@@@@@@             @@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@         @@@@@@@@@@@@@@@               @@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     @@@@@@@@@@@@@@@@                 @@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                  @@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                    @@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                      @@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.                         @
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                             @
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@,                                       @
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                                                @
// @@@@@@@@@@@@@@@@@@@@@@@@@@@                                                    @
// @@@@@@@@@@@@@@@@@@@@@@@@@                                                     @@
// @@@@@@@@@@@@@@@@@@@@@@@                                                       @@
// @@@@@@@@@@@@@@@@@@@@@@                                                       @@@
// @@@@@@@@@@@@@@@@@@@@@*                @@@@@                                 @@@@
// @@@@@@@@@@@@@@@@@@@@@               @@@@@@@@@                              @@@@@
// @@@@@@@@@@@@@@@@@@@@@              @@@@@@@@@@@                           @@@@@@@
// @@@@@@@@@@@@@@@@@@@@@               @@@@@@@@%                           @@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@                                                @@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@                                            @@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@                                        %@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@                                   @@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@                           @@@@@@@@@@@@@@@@@@@@@@@@
// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@                @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
// © YinYangAlgorithms

//@version=5
indicator('Machine Learning: Trend Lines [YinYangAlgorithms]', overlay=true)
// ~~~~~~~~~~~~ INPUTS ~~~~~~~~~~~~ //
useMLSources = input.bool(true, "Use Machine Learning Sources", tooltip="If disabled Traditional Trend line sources (High and Low) will be used rather than Rational Quadratics.")
useKNN = input.bool(true, "Use KNN Distance Sorting", tooltip="You can disable this if you wish to not have the Machine Learning Data sorted using KNN. If disabled trend line logic will be Traditional.")
useExponentialAverage = input.bool(true, "Use Exponential Data Average", tooltip="This Settings uses a custom Exponential Data Average of the KNN rather than simply averaging the KNN.")
//Notice: If any of these values are changed, you will likely need to 'Fine Tune' the rest as they all effect each other quite drastically 
    //and can lead to poor results if they aren't adjusted together accordingly.
mlLength = input.int(50, "Machine Learning Length", tooltip="How strong is our Machine Learning Memory? Please note, when this value is too high the data is almost 'too' much and can lead to poor results.")
knnLength = input.int(5, "K-Nearest Neighbour (KNN) Length", tooltip="How many K-Nearest Neighbours are allowed with our Distance Clustering? Please note, too high or too low may lead to poor results.")
projectionLength_fast = input.int(5, "Fast ML Data Length", tooltip="Fast and Slow speed needs to be adjusted properly to see results. 3/5/7 all seem to work well for Fast.")
projectionLength_slow = input.int(30, "Slow ML Data Length", tooltip="Fast and Slow speed needs to be adjusted properly to see results. 20 - 50 all seem to work well for Slow.")

// ~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~ //
var line upwardTrendLine = na
var line downwardTrendLine = na
var float topLine = na
var float botLine = na

// ~~~~~~~~~~~~ FUNCTIONS ~~~~~~~~~~~~ //
//Get the exponential average of an array, where the exponential weight is focused on the first value of this array (current source)
getExponentialDataAverage(_data, _length) =>
    avg = 0.
    maxLen = math.min(_data.size(), _length)
    if maxLen > 0
        for i = 0 to maxLen - 1
            curData = _data.get(i)
            tempAvg = curData
            if i > 0
                for a = 0 to i
                    tempAvg += array.get(_data, a)
                tempAvg := math.avg(_data.get(0), tempAvg / i)
            avg += math.avg(_data.get(0), math.avg(curData, tempAvg))
        avg / _length
    else
        avg

//Uses KNN to sort distances with our ML fast and slow data
//This is a modified version that sorts distances but rather than saving and outputting the distance average it outputs the mean of the valid distance and averages the total
knnAverage_fromDistance(_dataFast, _dataSlow, _minDist, _maxDist) =>
    //failsafe we need at least 1 distance
    maxDist = not _minDist ? true : _maxDist
    //Calculate the distance between slow and fast moving MIF
    distances = array.new_float(0)
    for i = 0 to _dataSlow.size() - 1
        distance = _dataSlow.get(i) - _dataFast.get(i)
        distances.push(distance)
    //clone the array so it doesn't modify it
    clonedDistances = distances.copy()    
    //slice the length from the array and calculate the max value from this slice
    splicedDistances = clonedDistances.slice(0, math.min(knnLength, clonedDistances.size()))
    maxDistanceAllowed = splicedDistances.max()
    minDistanceAllowed = splicedDistances.min()
    //scan all distances and add any that are less than max distance
    validDistances = array.new_float(0)
    for i = 0 to distances.size() - 1
        if (not maxDist or distances.get(i) <= maxDistanceAllowed) and (not _minDist or distances.get(i) >= minDistanceAllowed)
            distAvg = (_dataSlow.get(i) + _dataFast.get(i)) / 2
            validDistances.push(distAvg)

    //Get exponential or regular average
    if useExponentialAverage
        getExponentialDataAverage(validDistances, 1)
    else
        validDistances.avg()

//@jdehorty Kernel Function
//used to turn a source into a rational quadratic which performs better in ML calculations
rationalQuadratic(_src, _lookback, _relativeWeight, _startAtBar) =>
	float _currentWeight = 0.
	float _cumulativeWeight = 0.
	_size = array.size(array.from(_src))
    for i = 0 to _size + _startAtBar
        y = _src[i]
        w = math.pow(1 + (math.pow(i, 2) / ((math.pow(_lookback, 2) * 2 * _relativeWeight))), -_relativeWeight)
        _currentWeight += y*w
        _cumulativeWeight += w
    yhat = _currentWeight / _cumulativeWeight
    yhat

// ~~~~~~~~~~~~ CALCULATIONS ~~~~~~~~~~~~ //
//Get High and Low sources
mlHigh = rationalQuadratic(high, 8, 8., 25)
mlLow = rationalQuadratic(low, 8, 8., 25)
//Define which source type is used
highSource = useMLSources ? mlHigh : high
lowSource = useMLSources ? mlLow : low
//calculate our length and bar average length
trendLine_length = projectionLength_fast + projectionLength_slow + 1
trendLine_barAvg = (projectionLength_fast + projectionLength_slow) / 2

// ---- Highest (Downward Trend Lines) ---- //
//Get out Slow and Fast moving ML Lengths
int mlHighestbar_fast = ta.highestbars(highSource, projectionLength_fast * 2 + 1)
int mlHighestbar_slow = ta.highestbars(highSource, projectionLength_slow * 2 + 1)
//Create the ml storage
mlHighestBars_fast = array.new_int(mlLength)
mlHighestBars_slow = array.new_int(mlLength)
//populate slow and fast ML storage for downward trend lines
for h = 0 to mlLength - 1
    mlHighestBars_fast.set(h, mlHighestbar_fast[h])
    mlHighestBars_slow.set(h, mlHighestbar_slow[h])
//calculate the ml highest bar using KNN
highestbars_1 = useKNN ? knnAverage_fromDistance(mlHighestBars_fast, mlHighestBars_slow, true, true) : ta.highestbars(highSource, trendLine_length)
//check if we have a new highest bar
iff_1 = highestbars_1 == -trendLine_barAvg ? highSource[trendLine_barAvg] : na
topLine := not na(highSource[trendLine_length]) ? iff_1 : na
//create a new downward trend line when we have a new topLine
lineTop = ta.valuewhen(topLine, topLine, 1)
trend_startTop = 0
trend_startTop := topLine ? 1 : nz(trend_startTop[1]) + 1
var float topAngle = 0.0
topAngle := topAngle[1]
if not na(lineTop) and not na(topLine)
    if lineTop > topLine
        downwardTrendLine := line.new(bar_index - trend_startTop[1] - trendLine_barAvg, highSource[trend_startTop[1] + trendLine_barAvg], bar_index - trendLine_barAvg, highSource[trendLine_barAvg], color=color.red, extend=extend.right)
        topAngle := (highSource[trend_startTop[1] + trendLine_barAvg] - highSource[trendLine_barAvg]) / trend_startTop[1]
    if lineTop <= topLine
        topAngle := 0.0

// ---- Lowest (Upward Trend Lines) ---- //
//Get out Slow and Fast moving ML Lengths
int mlLowestbar_fast = ta.lowestbars(lowSource, projectionLength_fast * 2 + 1)
int mlLowestbar_slow = ta.lowestbars(lowSource, projectionLength_slow * 2 + 1)
//Create the ml storage
mlLowestBars_fast = array.new_int(mlLength)
mlLowestBars_slow = array.new_int(mlLength)
//populate slow and fast ML storage for upward trend lines
for l = 0 to mlLength - 1
    mlLowestBars_fast.set(l, mlHighestbar_fast[l])
    mlLowestBars_slow.set(l, mlHighestbar_slow[l])
//calculate the ml lowest bar using KNN
lowestbars_1 = useKNN ? knnAverage_fromDistance(mlLowestBars_fast, mlLowestBars_slow, true, true) : ta.lowestbars(lowSource, trendLine_length)
//check if we have a new lowest bar
iff_2 = lowestbars_1 == -trendLine_barAvg ? lowSource[trendLine_barAvg] : na
botLine := not na(lowSource[trendLine_length]) ? iff_2 : na
//create a new upward trend line when we have a new botline
lineBottom = ta.valuewhen(botLine, botLine, 1)
trend_startBot = 0
trend_startBot := botLine ? 1 : nz(trend_startBot[1]) + 1
var float botAngle = 0.0
botAngle := botAngle[1]
if not na(lineBottom) and not na(botLine)
    if lineBottom < botLine
        upwardTrendLine := line.new(bar_index - trend_startBot[1] - trendLine_barAvg, lowSource[trend_startBot[1] + trendLine_barAvg], bar_index - trendLine_barAvg, lowSource[trendLine_barAvg], color=color.green, extend=extend.right)
        botAngle := (lowSource[trend_startBot[1] + trendLine_barAvg] - lowSource[trendLine_barAvg]) / trend_startBot[1]
    if lineBottom >= botLine
        botAngle := 0.0

// ~~~~~~~~~~~~ END ~~~~~~~~~~~~ //