Machine Learning: VWAP [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: VWAP [YinYangAlgorithms]", overlay=true)
// ~~~~~~~~~~~~ INPUTS ~~~~~~~~~~~~ //
//VWAP
vwapType = input.string("Bullish", "VWAP Type", options=["Bullish", "Bearish", "Neutral"], group="VWAP Zones", tooltip="Type of VWAP. You can favor specific direction changes or let it be Neutral where there is even weight to both. Please note, these do not apply to the Machine Learning VWAP.")
src = input.source(ohlc4, "Source", group="VWAP Zones", tooltip="VWAP Source. By default VWAP usually uses HLC3; however OHLC4 may help by providing more data.")
length = input.int(100, "Lookback Length", group="VWAP Zones", tooltip="The Length of this VWAP when it comes to seeing if the current High > Highest of this length; or if the current Low is < Lowest of this length.")
vwmaRegularMult = input.float(2.0, "Standard VWAP Multiplier", group="VWAP Zones", tooltip="This multiplier is applied only to the Standard VWMA. This is when 'Machine Learning Type' is set to 'None'.")
//Machine Learning
useRationalQuadratics = input.bool(true, "Use Rational Quadratics", group="Machine Learning", tooltip="Rationalizing our source may be beneficial for usage within ML calculations.")
showSignals = input.string("High / Low", "Signal Type", group="Machine Learning", options=["High / Low", "Close", "None"], tooltip="Bullish and Bearish Signals are when the price crosses over/under the basis, as well as the Upper and Lower levels. These may act as indicators to where price movement may occur.")
useMachineLearning = input.string("Simple Average", "Machine Learning Type", options=["KNN Average", "KNN Exponential Average", "Simple Average", "None"], group="Machine Learning", tooltip="Are we using a Simple ML Average, KNN Mean Average, KNN Exponential Average or None?")
distanceType = input.string("Both", "KNN Distance Type", options=["Both", "Max", "Min"], group="Machine Learning", tooltip="We need to check if distance is within the KNN Min/Max distance, which distance checks are we using.")
mlLength = input.int(10, "Machine Learning Length", group="Machine Learning", tooltip="How far back is our Machine Learning going to keep data for.")
knnLength = input.int(3, "k-Nearest Neighbour (KNN) Length", group="Machine Learning", tooltip="How many k-Nearest Neighbours will we account for?")
fastLength = input.int(1, "Fast ML Data Length", group="Machine Learning", tooltip="What is our Fast ML Length?? This is used with our Slow Length to create our KNN Distance.")
slowLength = input.int(5, "Slow ML Data Length", group="Machine Learning", tooltip="What is our Slow ML Length?? This is used with our Fast Length to create our KNN Distance.")
vwmaMult = input.float(3.236, "ML VWAP Multiplier", group="Machine Learning", tooltip="This multiplier is applied only to the Machine Learning VWMA")

// ~~~~~~~~~~~~ VARIABLES ~~~~~~~~~~~~ //
distanceTypeMin = distanceType == "Min" or distanceType == "Both"
distanceTypeMax = distanceType == "Max" or distanceType == "Both"

// ~~~~~~~~~~~~ FUNCTIONS ~~~~~~~~~~~~ //
//@jdehorty Kernel Function
//used to turn a source into a rational quadratic which performs better in ML calculations
rationalQuadratic(series float _src, simple int _lookback, simple float _relativeWeight, simple int 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

//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 ML Data
    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 useMachineLearning == "KNN Exponential Average"
        getExponentialDataAverage(validDistances, 1)
    else
        validDistances.avg()

// ~~~~~~~~~~~~ CALCULATIONS ~~~~~~~~~~~~ //
//variable to save starting position of VWAPs
float start = na
// changeVwap = high > ta.highest(length)[1] or close < start[1]// or 
changeVwap = low < ta.lowest(length)[1] or close < start[1]// or high > ta.highest(length)[1]
// changeVwap = low < ta.lowest(length)[1] or high > ta.highest(length)[1]//close < start[1]// or high > ta.highest(length)[1]
changeVwapBear = high > ta.highest(length)[1] or close > start[1]
//Calculate Bullish and Bearish VWAP
[basis, upper, lower] = ta.vwap(src, changeVwap, vwmaRegularMult)
[basisBear, upperBear, lowerBear] = ta.vwap(src, changeVwapBear, vwmaRegularMult)
//Calculate start based on the VWAP Type
if vwapType == "Bullish"
    start := changeVwap or changeVwapBear ? basis : start[1]
else if vwapType == "Bearish"
    start := changeVwap or changeVwapBear ? basisBear : start[1]
else
    start := changeVwap ? basis : changeVwapBear ? basisBear : start[1]
//Define if Bearish or Bullish on this VWAP Change
isBear = start < start[1]

//Define Machine Learning VWAP Anchor Variable
bool basisChange = false

//This is used for all forms of Machine Learning, it is used to check if the ML VWAP anchor has changed
if useMachineLearning != "None"
    //potentially rationalize our source
    if useRationalQuadratics
        src := rationalQuadratic(src, 8, 8., 25)
    //define our storage arrays for ML
    basisData = array.new_float(0)
    upperData = array.new_float(0)
    lowerData = array.new_float(0)
    mlStart = array.new_float(mlLength)
    //Calculate VWAPS with a length of ML
    for i = 0 to mlLength - 1
        //check for ml change
        mlChange_bull = low[i] < ta.lowest(length + i)[1]
        mlChange_bear = high[i] > ta.highest(length + i)[1]
        //calculate vwap if there has been change
        [ml_basis_bull, ml_upper_bull, ml_lower_bull] = ta.vwap(src[i], mlChange_bull, vwmaMult)
        [ml_basis_bear, ml_upper_bear, ml_lower_bear] = ta.vwap(src[i], mlChange_bear, vwmaMult)
        //update start
        float mlStartTemp = na
        if vwapType == "Bullish"
            mlStartTemp := mlChange_bull or mlChange_bear ? ml_basis_bull : mlStart.get(i)[1]
        else if vwapType == "Bearish"
            mlStartTemp := mlChange_bull or mlChange_bear ? ml_basis_bear : mlStart.get(i)[1]
        else
            mlStartTemp := mlChange_bull ? ml_basis_bull : mlChange_bear ? ml_basis_bear : mlStart.get(i)[1]
        mlStart.set(i, mlStartTemp)
        //check if it is bullish or bearish
        mlIsBear = mlStart.get(i) < mlStart.get(i)[1]
        //populate ml data based on bullish/bearish direction
        basisData.push(mlIsBear ? ml_basis_bear : ml_basis_bull)
        upperData.push(mlIsBear ? ml_upper_bear : ml_upper_bull)
        lowerData.push(mlIsBear ? ml_lower_bear : ml_lower_bull)
    //create the locations based on the average
    basis := basisData.avg()
    upper := upperData.avg()
    lower := lowerData.avg()
    //check if the basis has changed (the VWAP anchor)
    basisChange := basis != basis[1] or not na(basis)

//Calculate the ML VWAP
[mlBasis, mlUpper, mlLower] = ta.vwap(src, basisChange, vwmaMult)

//Use our previous Simple ML VWAP and convert it into a KNN or KNN Exponential VWAP
if useMachineLearning == "KNN Average" or useMachineLearning == "KNN Exponential Average"
    // -- A still simple but more complex approach to Machine Learning using KNN to sort validDistances
    //Calculate our fast and slow MA's based on the current Simple Average ML data
    float vwapFast_basis = ta.sma(mlBasis, fastLength)
    float vwapSlow_basis = ta.sma(mlBasis, slowLength)
    float vwapFast_upper = ta.sma(mlUpper, fastLength)
    float vwapSlow_upper = ta.sma(mlUpper, slowLength)
    float vwapFast_lower = ta.sma(mlLower, fastLength)
    float vwapSlow_lower = ta.sma(mlLower, slowLength)
    //create storage data for ML lookbacks
    vwapFastData_basis = array.new_float(mlLength)
    vwapSlowData_basis = array.new_float(mlLength)
    vwapFastData_upper = array.new_float(mlLength)
    vwapSlowData_upper = array.new_float(mlLength)
    vwapFastData_lower = array.new_float(mlLength)
    vwapSlowData_lower = array.new_float(mlLength)
    //populate our ML storage with lookbacks at our Fast and Slow vwapSlow_lower
    for i = 0 to mlLength - 1
        vwapFastData_basis.set(i, vwapFast_basis[i])
        vwapSlowData_basis.set(i, vwapSlow_basis[i])
        vwapFastData_upper.set(i, vwapFast_upper[i])
        vwapSlowData_upper.set(i, vwapSlow_upper[i])
        vwapFastData_lower.set(i, vwapFast_lower[i])
        vwapSlowData_lower.set(i, vwapSlow_lower[i])
    //calculate our new ML VWAP using KNN by using distances within KNN min/max and filtering by direction
    mlBasis := knnAverage_fromDistance(vwapFastData_basis, vwapSlowData_basis, distanceTypeMin, distanceTypeMax)
    mlUpper := knnAverage_fromDistance(vwapFastData_upper, vwapSlowData_upper, distanceTypeMin, distanceTypeMax)
    mlLower := knnAverage_fromDistance(vwapFastData_lower, vwapSlowData_lower, distanceTypeMin, distanceTypeMax)

//calculate VWAP color
vwapColor = (useMachineLearning != "None" and mlBasis > mlBasis[1]) or (not isBear and useMachineLearning == "None") ? color.green : color.red

//calculate our Bull and Bear signals
basis_bullSignal = showSignals == "High / Low" ? ta.crossover(low, mlBasis) : ta.crossover(close, mlBasis)
basis_bearSignal = showSignals == "High / Low" ? ta.crossunder(high, mlBasis) : ta.crossunder(close, mlBasis)
upper_bearSignal = showSignals == "High / Low" ? ta.crossunder(high, mlUpper) : ta.crossunder(close, mlUpper)
lower_bullSignal = showSignals == "High / Low" ? ta.crossover(low, mlLower) : ta.crossover(close, mlLower)

// ~~~~~~~~~~~~ PLOTS ~~~~~~~~~~~~ //
//Plot Bull and Bear signals
plotshape(showSignals != "None" and basis_bullSignal, title="Basis Bull", color=color.green, style=shape.triangleup, location=location.belowbar, size=size.tiny)
plotshape(showSignals != "None" and lower_bullSignal, title="Lower Bull", color=color.green, style=shape.triangleup, location=location.belowbar, size=size.tiny)
plotshape(showSignals != "None" and basis_bearSignal, title="Basis Bear", color=color.red, style=shape.triangledown, location=location.abovebar, size=size.tiny)
plotshape(showSignals != "None" and upper_bearSignal, title="Upper Bear", color=color.red, style=shape.triangledown, location=location.abovebar, size=size.tiny)

//Plot Machine Learning VWAP
plot(useMachineLearning != "None" ? mlBasis : na, "Basis", color=#FF6D00)
p1 = plot(useMachineLearning != "None" ? mlUpper : na, "Upper", color=vwapColor)
p2 = plot(useMachineLearning != "None" ? mlLower : na, "Lower", color=vwapColor)
fill(p1, p2, title = "Background", color=color.new(vwapColor, 97))

//Plot regular VWAP based on if it is Bullish or Bearish
plot(useMachineLearning == "None" ? isBear ? basisBear : basis : na, "Basis", color=#FF6D00)
p3 = plot(useMachineLearning == "None" ? isBear ? upperBear : upper : na, "Upper", color=vwapColor)
p4 = plot(useMachineLearning == "None" ? isBear ? lowerBear : lower : na, "Lower", color=vwapColor)
fill(p3, p4, title = "Background", color=color.new(vwapColor, 97))

// ~~~~~~~~~~~~ ALERTS ~~~~~~~~~~~~ //
//alerts based on Bull and Bear cross'
alertcondition(basis_bullSignal, title="Basis Bull Signal", message="Basis Bull Signal")
alertcondition(lower_bullSignal, title="Lower Bull Signal", message="Lower Bull Signal")
alertcondition(basis_bearSignal, title="Basis Bear Signal", message="Basis Bear Signal")
alertcondition(upper_bearSignal, title="Upper Bear Signal", message="Upper Bear Signal")

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