LengthAdaptation
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © lastguru

//@version=5
// @description Collection of dynamic length adaptation algorithms. Mostly from various Adaptive Moving Averages (they are usually just EMA otherwise). Now you can combine Adaptations with any other Moving Averages or Oscillators (see my other libraries), to get something like Deviation Scaled RSI or Fractal Adaptive VWMA. This collection is not encyclopaedic. Suggestions are welcome.
library("LengthAdaptation", true)
import lastguru/CommonFilters/1 as cf
import lastguru/NormalizedOscillators/9 as co

// ----------
// Length Adaptations
// ----------

// @function Chande's Dynamic Length
// @param src Series to use
// @param len Reference lookback length
// @param sdlen Lookback length of Standard deviation
// @param smooth Smoothing length of Standard deviation
// @param power Exponent of the length adaptation (lower is smaller variation)
// @returns Calculated period
// Taken from Chande's Dynamic Momentum Index (CDMI or DYMOI), which is dynamic RSI with this length
// Original default power value is 1, but I use 0.5
// A variant of this algorithm is also included, where volume is used instead of price
export chande(float src, int len, int sdlen = 5, int smooth = 10, float power = 0.5) =>
    chandeSd = ta.stdev(src, sdlen)
    chandeV = chandeSd / cf.sma(chandeSd, smooth)
    chandeT = len / math.pow(chandeV, power)
    chandeT

// @function Variable Index Dynamic Average Indicator (VIDYA)
// @param src Series to use
// @param len Reference lookback length
// @param dynLow Lower bound for the dynamic length
// @returns Calculated period
// Standard VIDYA algorithm. The period oscillates from the Lower Bound up (slow)
// I took the adaptation part, as it is just an EMA otherwise
export vidya(float src, int len, int dynLow) =>
    rsiUp = cf.sma(math.max(ta.change(src), 0), len)
    rsiDown = cf.sma(-math.min(ta.change(src), 0), len)
    cmo = co.rsi2(rsiUp, rsiDown)
    alpha = (2 / (dynLow + 1)) * math.abs(cmo)
    nl = alpha != 0 ? (2 / alpha) - 1 : 0
    nl

// @function Relative Strength Dynamic Length - VIDYA RS
// @param src Series to use
// @param len Reference lookback length
// @param dynLow Lower bound for the dynamic length
// @param dynHigh Upper bound for the dynamic length
// @returns Calculated period
// Based on Vitali Apirine's modification (Stocks and Commodities, January 2022) of VIDYA algorithm. The period oscillates from the Upper Bound down (fast)
// I took the adaptation part, as it is just an EMA otherwise
// There was originally a multiplier for variation variable (RSI). It is replaced by lower bound, which is recalculated into the multiplier
export vidyaRS(float src, int len, int dynLow, int dynHigh) =>
    rsiUp = cf.ema(math.max(ta.change(src), 0), len)
    rsiDown = cf.ema(-math.min(ta.change(src), 0), len)
    rsi = co.rsi2(rsiUp, rsiDown)
    mult = ((dynHigh + 1) / (dynLow + 1)) - 1
    alpha = (2 / (dynHigh + 1))  * (1 + mult * math.abs(rsi))
    nl = alpha != 0 ? (2 / alpha) - 1 : 0
    nl

// @function Kaufman Efficiency Scaling
// @param src Series to use
// @param len Reference lookback length
// @param dynLow Lower bound for the dynamic length
// @param dynHigh Upper bound for the dynamic length
// @returns Calculated period
// Based on Efficiency Ratio calculation orifinally used in Kaufman Adaptive Moving Average developed by Perry J. Kaufman
// I took the adaptation part, as it is just an EMA otherwise
export kaufman(float src, int len, int dynLow, int dynHigh) =>
    er = math.abs(co.er(src, len))
    fa = 2 / (dynLow + 1)
    sa = 2 / (dynHigh + 1)
    ka = math.pow((fa - sa) * er + sa, 2)
    nl = ka != 0 ? (2 / ka) - 1 : 0
    nl

// @function Deviation Scaling
// @param src Series to use
// @param len Reference lookback length
// @returns Calculated period
// Based on Derivation Scaled Super Smoother (DSSS) by John F. Ehlers
// Originally used with Super Smoother
// RMS originally has 50 bar lookback. Changed to 4x length for better flexibility. Could be wrong.
export ds(float src, int len) =>
    mom = src - src[len]
    hlen = math.round(len/math.sqrt(2))
    filt = cf.hannFIR(mom, hlen)
    rms = co.rms(filt, len * 4)
    scfilt = rms != 0 ? filt/rms : 0
    nl = len / math.abs(scfilt)
    nl

// @function Median Average Adaptation
// @param src Series to use
// @param len Reference lookback length
// @param threshold Adjustment threshold (lower is smaller length, default: 0.002, min: 0.0001)
// @returns Calculated period
// Based on Median Average Adaptive Filter by John F. Ehlers
// Discovered and implemented by @cheatcountry: https://www.tradingview.com/script/pq6FWb3Y-Ehlers-Median-Average-Adaptive-Filter-CC/
// I took the adaptation part, as it is just an EMA otherwise
export maa(float src, int len, float threshold = 0.002) =>
    v3 = 0.2
    v2 = 0.0
    alpha = 0.0
    length = len
    while (v3 > threshold and length > 0)
        alpha := 2.0 / (length + 1)
        v1 = ta.median(src, length)
        v2 := (alpha * src) + ((1 - alpha) * nz(v2[1]))
        v3 := v1 != 0 ? math.abs(v1 - v2) / v1 : v3
        length -= 2
    
    nl = length < 3 ? 3 : length
    nl

// @function Fractal Adaptation
// @param len Reference lookback length
// @param fc Fast constant (default: 1)
// @param sc Slow constant (default: 200)
// @returns Calculated period
// Based on FRAMA by John F. Ehlers
// Modified to allow lower and upper bounds by an unknown author
// I took the adaptation part, as it is just an EMA otherwise
export fra(int len, int fc, int sc) =>
    float result = 0
    int len1 = math.floor(len/2)
    w = math.log(2/(sc+1)) / math.log(math.e) // Natural logarithm (ln(2/(SC+1)))
    h1 = ta.highest(high,len1)
    l1 = ta.lowest(low,len1)
    n1 = (h1-l1)/len1
    h2 = h1[len1]
    l2 = l1[len1]
    n2 = (h2-l2)/len1
    h3 = ta.highest(high,len)
    l3 = ta.lowest(low,len)
    n3 = (h3-l3)/len
    dimen1 = (math.log(n1+n2)-math.log(n3))/math.log(2)
    dimen = (n1>0 and n2>0 and n3>0) ? dimen1 : nz(dimen1[1])
    alpha1 = math.exp(w*(dimen-1))
    oldAlpha = alpha1>1?1:(alpha1<0.01?0.01:alpha1)
    oldN = (2-oldAlpha)/oldAlpha
    nl = (((sc-fc)*(oldN-1))/(sc-1))+fc
    nl

// @function MESA Adaptation - MAMA Alpha
// @param src Series to use
// @param dynLow Lower bound for the dynamic length
// @param dynHigh Upper bound for the dynamic length
// @returns Calculated period
// Based on MESA Adaptive Moving Average by John F. Ehlers
// Introduced in the September 2001 issue of Stocks and Commodities
// Inspired by the @everget implementation: https://www.tradingview.com/script/aaWzn9bK-Ehlers-MESA-Adaptive-Moving-Averages-MAMA-FAMA/
// I took the adaptation part, as it is just an EMA otherwise
export mama(float src, int dynLow, int dynHigh) =>
    period = 0.0
    C1 = 0.0962
    C2 = 0.5769
    C3 = 0.075 * nz(period[1]) + 0.54
    
    smooth = (4 * src + 3 * nz(src[1]) + 2 * nz(src[2]) + nz(src[3])) / 10
    detrend = C3 * (C1 * smooth + C2 * nz(smooth[2]) - C2 * nz(smooth[4]) - C1 * nz(smooth[6]))

    // Compute InPhase and Quadrature components
    Q1 = C3 * (C1 * detrend + C2 * nz(detrend[2]) - C2 * nz(detrend[4]) - C1 * nz(detrend[6]))
    I1 = nz(detrend[3])

    // Advance Phase of I1 and Q1 by 90 degrees
    jI = C3 * (C1 * I1 + C2 * nz(I1[2]) - C2 * nz(I1[4]) - C1 * nz(I1[6]))
    jQ = C3 * (C1 * Q1 + C2 * nz(Q1[2]) - C2 * nz(Q1[4]) - C1 * nz(Q1[6]))

    // Phasor addition for 3 bar averaging
    I2 = I1 - jQ
    Q2 = Q1 + jI

    // Smooth I and Q components before applying discriminator
    I2 := 0.2*I2 + 0.8*nz(I2[1])
    Q2 := 0.2*Q2 + 0.8*nz(Q2[1])

    // Extract Homodyne Discriminator
    Re = I2*nz(I2[1]) + Q2*nz(Q2[1])
    Im = I2*nz(Q2[1]) - Q2*nz(I2[1])
    Re  := 0.2*Re + 0.8*nz(Re[1])
    Im  := 0.2*Im + 0.8*nz(Im[1]) 

    period := (Re != 0 and Im != 0) ? 2 * math.pi / math.atan(Im/Re) : 0
    period := math.min(period, 1.5 * nz(period[1], period))
    period := math.max(period, (2/3) * nz(period[1], period))
	period := math.min(math.max(period, dynLow), dynHigh)
    period := period*0.2 + nz(period[1])*0.8
    
    phase = I1 != 0 ? (180/math.pi) * math.atan(Q1 / I1) : 0
    deltaPhase = phase[1] - phase
    deltaPhase := math.max(deltaPhase, 1)
    
    fa = 2 / (dynLow + 1)
    sa = 2 / (dynHigh + 1)
    alpha = math.max(fa / deltaPhase, sa)
    nl = (2 / alpha) - 1
    nl


// ----------
// Wrappers
// ----------

// @function Execute a particular Length Adaptation from the list
// @param type Length Adaptation type to use
// @param src Series to use
// @param len Reference lookback length
// @param dynLow Lower bound for the dynamic length
// @param dynHigh Upper bound for the dynamic length
// @param chandeSDLen Lookback length of Standard deviation for Chande's Dynamic Length
// @param chandeSmooth Smoothing length of Standard deviation for Chande's Dynamic Length
// @param chandePower Exponent of the length adaptation for Chande's Dynamic Length (lower is smaller variation)
// @returns Calculated period (float, not limited)
export doAdapt(string type, float src, int len, int dynLow, int dynHigh, int chandeSDLen = 5, int chandeSmooth = 10, float chandePower = 0.5) =>
    out = 0.0
    switch type
        "None" =>
            out := len
        "Chande (Price)" =>
            out := chande(src, len, chandeSDLen, chandeSmooth, chandePower)
        "Chande (Volume)" =>
            out := chande(volume, len, chandeSDLen, chandeSmooth, chandePower)
        "VIDYA" =>
            out := vidya(src, len, dynLow)
        "VIDYA-RS" =>
            out := vidyaRS(src, len, dynLow, dynHigh)
        "Kaufman Efficiency Scaling" =>
            out := kaufman(src, len, dynLow, dynHigh)
        "Deviation Scaling" =>
            out := ds(src, len)
        "Median Average" =>
            out := maa(src, dynHigh)
        "Fractal Adaptation" =>
            out := fra(len, dynLow, dynHigh)
        "MESA MAMA Alpha" =>
            out := mama(src, dynLow, dynHigh)
    
    out

// @function MA wrapper on wrapper: if DSSS is selected, calculate it here
// @param type MA type to use
// @param src Series to use
// @param len Filtering length
// @returns Filtered series
// Demonstration of a combined indicator: Deviation Scaled Super Smoother, Relative Strength Super Smoother
export doMA(string type, float src, int len) =>
    out = 0.0
    switch type
        "DSSS" =>
            out := cf.supers2(src, math.round(ds(src, len)))
        "RSSS" =>
            dynLow = math.round((len + 1) / (10 + 1)) - 1
            nl = math.round(vidyaRS(src, len, dynLow, len))
            out := cf.supers2(src, nl)
        =>
            out := cf.doMA(type, src, len)
    
    out
