NormalizedOscillators
// 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 some common Oscillators. All are zero-mean and normalized to fit in the -1..1 range. Some are modified, so that the internal smoothing function could be configurable (for example, to enable Hann Windowing, that John F. Ehlers uses frequently). Some are modified for other reasons (see comments in the code), but never without a reason. This collection is neither encyclopaedic, nor reference, however I try to find the most correct implementation. Suggestions are welcome.
library("NormalizedOscillators", true)
import lastguru/CommonFilters/1 as cf
import nolantait/HurstExponent/1 as he

// ----------
// Common functions
// ----------

// @function RSI - second step
// @param upper Upwards momentum
// @param lower Downwards momentum
// @returns Oscillator value
// Modified by Ehlers from Wilder's implementation to have a zero mean (oscillator from -1 to +1)
// Originally: 100.0 - (100.0 / (1.0 + upper / lower))
// Ignoring the 100 scale factor, we get: upper / (upper + lower)
// Multiplying by two and subtracting 1, we get: (2 * upper) / (upper + lower) - 1 =  (upper - lower) / (upper + lower)
export rsi2(float upper, float lower) =>
    rsi = 0.0
    if (upper + lower) != 0
        rsi := (upper - lower) / (upper + lower)

// ----------
// Internal smoothing algorithm selection
_smooth(type, src, len, default) =>
    cf.doMA(type == "Default" or type == "Fast Default" ? default : type, src, type == "Fast Default" ? math.round(len/2) : len)


// @function Root mean square (RMS)
// @param src Source series
// @param len Lookback period
// Based on by John F. Ehlers implementation
export rms(float src, int len) =>
    rms = math.sum(math.pow(src, 2), len)
    rms := math.sqrt(nz(rms / len))


// ----------
// Oscillator postfilters
// ----------

// @function Inverse Fisher Transform
// @param src Source series
// @returns Normalized series
// Based on by John F. Ehlers implementation
// The input values have been multiplied by 2 (was "2*src", now "4*src") to force expansion - not compression
// The inputs may be further modified, if needed
export ift(float src) =>
    (math.exp(4*src)-1) / (math.exp(4*src)+1)

// @function Stochastic
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
export stoch(float src, int len) =>
    src1 = src + 1
    max = ta.highest(src1, len)
    min = ta.lowest(src1, len)
    stoch = max != min ? (2 * src1 - max - min) / (max - min) : 0

// @function Super Smooth Stochastic (part of MESA Stochastic) by John F. Ehlers
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
// Introduced in the January 2014 issue of Stocks and Commodities
// This is not an implementation of MESA Stochastic, as it is based on Highpass filter not present in the function (but you can construct it)
// This implementation is scaled by 0.95, so that Super Smoother does not exceed 1/-1
// I do not know, if this the right way to fix this issue, but it works for now
export ssstoch(float src, int len) =>
    src1 = src + 1
    max = ta.highest(src1, len)
    min = ta.lowest(src1, len)
    stoch = max != min ? 0.95 * (2 * src1 - max - min) / (max - min) : 0
    cf.supers2(stoch, len)

// @function Noise Elimination Technology by John F. Ehlers
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
// Introduced in the December 2020 issue of Stocks and Commodities
// Uses simplified Kendall correlation algorithm
// Implementation by @QuantTherapy: https://www.tradingview.com/script/4wdh1Y1s-NET-MyRSI/
export netKendall(float src, int len) =>
    denom = len * (len - 1) / 2
    num = 0.0 
    for i = 1 to len - 1
        for k = 0 to i - 1
            num := num - math.sign(src[i] - src[k])
    num/denom

// @function Momentum
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
// Derivative of the oscillator series with IFT normalization to force the -1..1 range
export momentum(float src, int len) =>
    mom = src - src[len]
    ift(mom)

// ----------
// Oscillators
// ----------

// @function RSI
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
export rsi(float src, int len, string smooth) =>
    default = "RMA"
    rsiUp = _smooth(smooth, math.max(ta.change(src), 0), len, default)
    rsiDown = _smooth(smooth, -math.min(ta.change(src), 0), len, default)
    rsi2(rsiUp, rsiDown)

// @function Volume-scaled RSI
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// This is my own version of RSI. It scales price movements by the proportion of RMS of volume
export vrsi(float src, int len, string smooth) =>
    default = "RMA"
    rsiUp = _smooth(smooth, math.max(ta.change(src), 0) * volume / rms(volume, len), len, default)
    rsiDown = _smooth(smooth, -math.min(ta.change(src), 0) * volume / rms(volume, len), len, default)
    rsi2(rsiUp, rsiDown)

// @function Momentum RSI
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Inspired by RocketRSI by John F. Ehlers (Stocks and Commodities, May 2018)
export mrsi(float src, int len, string smooth) =>
    default = "RMA"
    mom = src - src[len-1]
    rsiUp = _smooth(smooth, math.max(ta.change(mom), 0), len, default)
    rsiDown = _smooth(smooth, -math.min(ta.change(mom), 0), len, default)
    rsi2(rsiUp, rsiDown)

// @function Rocket RSI
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Inspired by RocketRSI by John F. Ehlers (Stocks and Commodities, May 2018)
// Does not include Fisher Transform of the original implementation, as the output must be normalized
// Does not include momentum smoothing length configuration, so always assumes half the lookback length
export rrsi(float src, int len, string smooth) =>
    default = "SMA"
    mom = src - src[len-1]
    filt = cf.supers2(mom, len/2)
    rsiUp = _smooth(smooth, math.max(ta.change(filt), 0), len, default)
    rsiDown = _smooth(smooth, -math.min(ta.change(filt), 0), len, default)
    rsi2(rsiUp, rsiDown)

// @function Money Flow Index
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
export mfi(float src, int len, string smooth) =>
    default = "SMA"
    mfiUp = _smooth(smooth, (ta.change(src) <= 0 ? 0 : src) * volume, len, default)
    mfiDown = _smooth(smooth, (ta.change(src) >= 0 ? 0 : src) * volume, len, default)
    rsi2(mfiUp, mfiDown)

// @function Laguerre RSI by John F. Ehlers
// @param src Source series
// @param in_gamma Damping factor (default is -1 to generate from len)
// @param len Lookback period (alternatively, if gamma is not set)
// @returns Oscillator series
// The original implementation is with gamma. As it is impossible to collect gamma in my system, where the only user input is length,
// an alternative calculation is included, where gamma is set by dividing len by 30. Maybe different calculation would be better?
export lrsi (float src, float in_gamma = -1, int len = 0) =>
    gamma = in_gamma == -1 ? len / 30 : in_gamma

    lrsi = 0.0
    L0 = 0.0
    L1 = 0.0
    L2 = 0.0
    L3 = 0.0
    
    L0 := (1 - gamma) * src + gamma * nz(L0[1])
    L1 := -gamma * L0 + nz(L0[1]) + gamma * nz(L1[1])
    L2 := -gamma * L1 + nz(L1[1]) + gamma * nz(L2[1])
    L3 := -gamma * L2 + nz(L2[2]) + gamma * nz(L3[1])
    
    cu = 0.0
    cd = 0.0
    if (L0 >= L1)
        cu := L0 - L1
    else
        cd := L1 - L0
    
    if (L1 >= L2)
        cu := cu + L1 - L2
    else
        cd := cd + L2 - L1
    
    if (L2 >= L3)
        cu := cu + L2 - L3
    else
        cd := cd + L3 - L2
    
    lrsi := rsi2(cu, cd)

// @function Choppiness Index or Fractal Energy
// @param len Lookback period
// @returns Oscillator series
// The Choppiness Index (CHOP) was created by E. W. Dreiss
// This indicator is sometimes called Fractal Energy
export fe (int len) =>
    sumtr = math.sum(ta.tr, len)
    fe = math.log(sumtr / (ta.highest(len) - ta.lowest(len))) / math.log(len)
    2 * fe - 1

// @function Efficiency ratio
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
// Based on Kaufman Adaptive Moving Average calculation
// This is the correct Efficiency ratio calculation, and most other implementations are wrong:
// the number of bar differences is 1 less than the length, otherwise we are adding the change outside of the measured range!
// For reference, see Stocks and Commodities June 1995
export er(float src, int len) =>
    signal = src - src[len]
    diff = math.abs(src - src[1])
    noise = math.sum(diff, len-1)
    er = noise != 0 ? signal / noise : 0

// @function Directional Movement Index
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Based on the original Tradingview algorithm
// Modified with inspiration from John F. Ehlers DMH (but not implementing the DMH algorithm!)
// Only ADX is returned
// Rescaled to fit -1 to +1
// Unlike most oscillators, there is no src parameter as DMI works directly with high and low values
export dmi (int len, string smooth) =>
    default = "RMA"
    up = ta.change(high)
    down = -ta.change(low)
    plusDM = na(up) ? na : (up > down and up > 0 ? up : 0)
    minusDM = na(down) ? na : (down > up and down > 0 ? down : 0)
    plus = fixnan(100 * cf.rma(plusDM, len))
    minus = fixnan(100 * cf.rma(minusDM, len))
    sum = plus + minus
    adx = _smooth(smooth, math.abs(plus - minus) / (sum == 0 ? 1 : sum), len, default)
    2 * adx - 1

// @function Fast Directional Movement Index
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Same as DMI, but without secondary smoothing. Can be smoothed later. Instead, +DM and -DM smoothing can be configured
export fdmi (int len, string smooth) =>
    default = "RMA"
    up = ta.change(high)
    down = -ta.change(low)
    plusDM = na(up) ? na : (up > down and up > 0 ? up : 0)
    minusDM = na(down) ? na : (down > up and down > 0 ? down : 0)
    plus = fixnan(100 * _smooth(smooth, plusDM, len, default))
    minus = fixnan(100 * _smooth(smooth, minusDM, len, default))
    sum = plus + minus
    adx = math.abs(plus - minus) / (sum == 0 ? 1 : sum)
    2 * adx - 1

// @function Average Vortex Index (AVX)
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Based on the Vortex Indicator. I then apply ADX calculation on the VI+ and VI- lines.
// Only AVX is returned
// Rescaled to fit -1 to +1
// Unlike most oscillators, there is no src parameter as AVX works directly with high and low values
export avx(int len, string smooth) =>
    default = "RMA"
    plusVM = math.sum(math.abs( high - low[1]), len)
    minusVM = math.sum(math.abs( low - high[1]), len)
    str = math.sum(ta.tr, len)
    plus = plusVM / str
    minus = minusVM / str
    sum = plus + minus
    adx = _smooth(smooth, math.abs(plus - minus) / (sum == 0 ? 1 : sum), len, default)
    4 * adx - 1

// @function Hurst Exponent
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
// Until I can write my own, I use the @nolantait library: https://www.tradingview.com/script/LsW08uRb-HurstExponent/
// Which in turn is based on the excellent @balipour implementation: https://www.tradingview.com/script/vTloluai-Hurst-Exponent-Detrended-Fluctuation-Analysis-pig/
// As with all other indicators, this is also rescaled to fit -1 to +1
export hurst(float src, int len) =>
    out = he.exponent(src, len)
    2 * out - 1

// @function Simple Fractal Dimension
// @param len Reference lookback length
// @returns Oscillator series
// Based on FRAMA by John F. Ehlers
// This function implements just the first part of FRAMA: calculating Fractal Dimension
// It is then transformed to Hurst Exponent (HE = 2 - FD) and normalized to fit -1 to +1
export fd(int len) =>
    float result = 0
    int hlen = math.floor(len / 2)
    h1 = ta.highest(high, hlen)
    l1 = ta.lowest(low, hlen)
    n1 = (h1 - l1) / hlen
    h2 = h1[hlen]
    l2 = l1[hlen]
    n2 = (h2 - l2) / hlen
    h3 = ta.highest(high, 2 * hlen)
    l3 = ta.lowest(low, 2 * hlen)
    n3 = (h3 - l3) / (2 * hlen)
    dimen1 = (math.log(n1 + n2) - math.log(n3)) / math.log(2)
    dimen = (n1 > 0 and n2 > 0 and n3 > 0) ? dimen1 : nz(dimen1[1])
    ift(2 * (2 - dimen) - 1)

// @function Finite Volume Element (FVE)
// @param len Lookback period
// @returns Oscillator series
// Based on FVE with Volatility adjustment by Markos Katsanos (Stocks and Commodities, September 2003)
export fve(int len) =>
    cintra = 0.1
    cinter = 0.1
    tp = hlc3
    
    intra = math.log(high) - math.log(low)
    vintra = ta.stdev(intra, len)
    inter = math.log(tp) - math.log(tp[1])
    vinter = ta.stdev(inter, len)
    cutoff = cintra * vintra + cinter * vinter
    mf = close - hl2 + tp - tp[1]
    
    fveFactor = mf > cutoff * close ? 1 : (mf < cutoff * close ? -1 : 0)
    volumePM = volume * fveFactor
    
    fveSum = ta.sma(volumePM, len)
    fveMA = ta.sma(volume, len)
    fve = fveSum / fveMA

// @function Volume Flow Indicator (VFI)
// @param len Lookback period
// @returns Oscillator series
// Based on VFI by Markos Katsanos (Stocks and Commodities, June 2004)
// https://mkatsanos.com/volume-flow-vfi-indicator/
// As with all other indicators, this is also rescaled to fit -1 to +1
export vfi (int len) =>
    // Cutoff Coefficient
    coef = 0.2
    // Volume Volatility Coefficient
    vcoef = 2.5
    
    inter = math.log(hlc3) - math.log(hlc3[1])
    vinter = ta.stdev(inter, 30)
    cutoff = coef * vinter * close
    vave = ta.sma(volume[1], len)
    vc = math.min(volume, vave * vcoef)
    mf = hlc3 - hlc3[1]
    vcp = mf > cutoff ? vc : mf < -cutoff ? -vc : 0
    vfi = math.sum(vcp, len) / vave
    ift(vfi / 20)

// @function Relative Vigor Index by John F. Ehlers
// @param len Lookback period
// @returns Oscillator series
// Introduced in the January 2002 issue of Stocks and Commodities
// Similar to A/D Oscillator
export rvi(int len) =>
    co = close - open
    hl = high - low
    num = cf.fir23(co)
    denom = cf.fir23(hl)
    snum = cf.sma(num, len)
    sdenom = cf.sma(denom, len)
    rvi = snum / sdenom

// ----------
// Oscillator wrappers
// ----------

// @function Execute a particular Oscillator from the list
// @param type Oscillator type to use
// @param src Source series
// @param len Lookback period
// @param smooth Internal smoothing algorithm
// @returns Oscillator series
// Chande Momentum Oscillator (CMO) is RSI without smoothing. No idea, why some authors use different calculations
// LRSI with Fractal Energy is a combo oscillator that uses Fractal Energy to tune LRSI gamma, as seen here: https://www.prorealcode.com/prorealtime-indicators/rsi-laguerre-adjusting-gamma-fractals-energy/
export doOsc(string type, float src, int len, string smooth) =>
    out = 0.0
    switch type
        "None" =>
            out := src
        "Stochastic" =>
            out := stoch(src, len)
        "Super Smooth Stochastic" =>
            out := ssstoch(src, len)
        "CMO" =>
            out := rsi(src, len, "SMA")
        "RSI" =>
            out := rsi(src, len, smooth)
        "Volume-scaled RSI" =>
            out := vrsi(src, len, smooth)
        "Momentum RSI" =>
            out := mrsi(src, len, smooth)
        "Rocket RSI" =>
            out := vrsi(src, len, smooth)
        "MFI" =>
            out := mfi(src, len, smooth)
        "LRSI" =>
            out := lrsi(src, -1, len)
        "LRSI with Fractal Energy" =>
            fe = (fe(len) + 1) / 2
            out := lrsi(src, fe, 0)
        "Fractal Energy" =>
            out := fe(len)
        "Efficiency Ratio" =>
            out := er(src, len)
        "DMI" =>
            out := dmi(len, smooth)
        "Fast DMI" =>
            out := fdmi(len, smooth)
        "AVX" =>
            out := avx(len, smooth)
        "Hurst Exponent" =>
            out := hurst(src, len)
        "Fractal Dimension" =>
            out := fd(len)
        "FVE" =>
            out := fve(len)
        "VFI" =>
            out := vfi(len)
        "RVI" =>
            out := rvi(len)
    
    out

// @function Execute a particular Oscillator Postfilter from the list
// @param type Oscillator type to use
// @param src Source series
// @param len Lookback period
// @returns Oscillator series
export doPostfilter(string type, float src, int len) =>
    out = 0.0
    switch type
        "None" =>
            out := src
        "Stochastic" =>
            out := stoch(src, len)
        "Super Smooth Stochastic" =>
            out := ssstoch(src, len)
        "Inverse Fisher Transform" =>
            out := ift(src)
        "Noise Elimination Technology" =>
            out := netKendall(src, len)
        "Momentum" =>
            out := momentum(src, len)
    
    out
