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

//@version=5

// @description Easy to use drop-in facade function to lots of different moving average calculations, including some that are not natively available in PineScript v5 such as Zero-Lag EMA. Simply call f_maSelect(series float serie, simple string ma_type="sma", ma_length=14) instead of a ta.*ma() call and you get access to all MAs offered by PineScript and more.
library("f_maSelect")

// Note this is inspired by my work in the indicator: https://www.tradingview.com/script/oRO7g5ze-Generalized-Bollinger-Bands-B-And-Bandwidth-Tartigradia/ and others before.

// @function Zero-lag EMA (ZLMA)
// @param src Input series
// @param len Lookback period
// @returns Series smoothed with ZLMA
export zema(series float src, int len) =>
    // by DreamsDefined in: https://www.tradingview.com/script/pFyPlM8z-Reverse-Engineered-RSI-Key-Levels-MTF/
    ema1 = ta.ema(src, len)
    ema2 = ta.ema(ema1, len)
    d = ema1 - ema2
    zlema = ema1 + d
    zlema

// @function Approximate Standard Moving Average, which substracts the average instead of popping the oldest element, hence losing the base frequency and is why it is approximative. For some reason, this appears to give the same results as a standard RMA
// @param x Input series.
// @param ma_length Lookback period.
// @returns Approximate SMA series.
export approximate_sma(series float x, int ma_length) =>
    sma = 0.0
    sma := nz(sma[1]) - (nz(sma[1] / ma_length)) + x
    sma

// @function Generalized moving average selector
// @param serie Input series
// @param ma_type String describing which moving average to use
// @param ma_length Lookback period
// @returns Serie smoothed with the selected moving average.
export f_maSelect(series float serie, simple string ma_type="sma", simple int ma_length=14) =>
    switch ma_type
        "sma" => ta.sma(serie, ma_length)
        "ema" => ta.ema(serie, ma_length)
        "rma" => ta.rma(serie, ma_length)
        "wma" => ta.wma(serie, ma_length)
        "vwma" => ta.vwma(serie, ma_length)
        "swma" => ta.swma(serie)
        "hma" => ta.hma(serie, ma_length)
        "alma" => ta.alma(serie, ma_length, 0.85, 6)
        "zlma" => zema(serie, ma_length)
        "approximate_sma" => approximate_sma(serie, ma_length)
        "median" => ta.percentile_nearest_rank(serie, ma_length, 50)
        =>
            runtime.error("No matching MA type found.")
            float(na)

//generalized_dev_vectorized(series float src, simple int length, simple string ma_type, simple int lmode=2) =>
    // If pinescript supported vectorization, we could write something like the following, but alas, it does not work as of pinescript v5, because src is not the vector but the current value, a scalar:
    //avg = f_maSelect(src, ma_type, length)
    //stdev = lmode == 1 ?
    //     f_maSelect(math.abs(src - avg), ma_type, length) :
    //     math.sqrt(f_maSelect(math.pow(src - avg, 2), ma_type, length))

// @function Generalized deviation calculation: Whereas other Bollinger Bands often just change the basis but not the stdev calculation, the correct way to change the basis is to also change it inside the stdev calculation.
// @param src Series to use (default: close)
// @param length Lookback period
// @param avg Average basis to use to calculate the standard deviation
// @param lmode L1 or L2 regularization? (ie, lmode=1 uses abs() to cutoff negative values hence it calculates the Mean Absolute Deviation as does the ta.dev(), lmode=2 uses sum of squares hence it calculates the true Standard Deviation as the ta.stdev() function does). See also the research works of everget: https://www.tradingview.com/script/qXc06HaE-RESEARCH-Mean-Absolute-Deviation/
// @returns stdev Standard deviation series
export generalized_dev(series float src, simple int length, float avg, simple int lmode=2) =>
    // Inspired by the official pine_stdev from the v5 manual: https://www.tradingview.com/pine-script-reference/v5/#fun_ta{dot}stdev
    sumOfSquareDeviations = 0.0
    for i = 0 to length - 1
        diff = src[i] - avg
        sumOfSquareDeviations := lmode == 1 ? sumOfSquareDeviations + math.abs(diff) : sumOfSquareDeviations + diff * diff
    stdev = lmode == 1 ? sumOfSquareDeviations / length : math.sqrt(sumOfSquareDeviations / length)

// @function Standard deviation calculation but with different probabilities assigned to each bar, with newer bars having more weights https://en.wikipedia.org/wiki/Standard_deviation#Discrete_random_variable
// @param src Series to use (default: close)
// @param length Lookback period
// @param avg Average basis to use to calculate the standard deviation
// @param lmode L1 or L2 regularization? (ie, lmode=1 uses abs() to cutoff negative values hence it calculates the Mean Absolute Deviation as does the ta.dev(), lmode=2 uses sum of squares hence it calculates the true Standard Deviation as the ta.stdev() function does). See also the research works of everget: https://www.tradingview.com/script/qXc06HaE-RESEARCH-Mean-Absolute-Deviation/
// @param temporal_discount Probabilistic gamma factor to discount old values in favor of new ones, higher value = more weight to newer bars
// @returns stdev Standard deviation series
export generalized_dev_discount(series float src, simple int length, float avg, simple int lmode=2, simple float temporal_discount=0.5) =>
    // We use an Exponential Smoothing to make the parametrization easier for the user, so that there is only a single parameter to change https://en.wikipedia.org/wiki/Exponential_smoothing
    sumOfSquareDeviations = 0.0
    for i = 0 to length - 1
        diff = src[i] - avg
        sumOfSquareDeviations := lmode == 1 ? (1-temporal_discount) * sumOfSquareDeviations + temporal_discount * math.abs(diff) : (1 - temporal_discount) * sumOfSquareDeviations + temporal_discount * diff * diff
    stdev = lmode == 1 ? sumOfSquareDeviations : math.sqrt(sumOfSquareDeviations)

// @function Median Absolute Deviation
// @param src Input series
// @param length Lookback period
// @param median Median already calculated on the input series
// @returns mad, the median absolute deviation value
export median_absdev(series float src, simple int length, float median) =>
    // median_absolute_deviation = median( abs(x_i - median(x)) )
    float [] sum = array.new_float(0)
    for i = 0 to length - 1
        diff = src[i] - median
        array.push(sum, math.abs(diff))
    mad = array.percentile_nearest_rank(sum, 50)

//
// Example, if library is added as an indicator
//

// Inputs
src = input.source(close, title="Source")
ma_length = input.int(20, title="Lookback period (length of smoothing)", minval=1, maxval=100)
ma_type = input.string('ema', 'Moving Average Type', options=['sma', 'ema', 'rma', 'wma', 'vwma', 'swma', 'hma', 'alma', 'zlma', 'approximate_sma', 'median'])
ma_color = input.string('source', 'Color average according to either the difference between bars (interbars) or difference between average and source?', options=['interbars','source','none'])
ma_margin = input.float(0.01, 'Margin to consider the cloud as undecided (in percent change)', tooltip='If the moving average value difference between two bars is below this percent, then we consider the direction as undecided. Set to 0 to disable margin.')

// Calculate average using our facade function
ma = f_maSelect(src, ma_type, ma_length)

// Calculate color
// Calculating the percent change from values of two different signs is surprisingly difficult: https://math.stackexchange.com/a/3986352/72736
// The total change may be off but we mainly want to see the sign so that's a good enough approximation.
final_color = switch ma_color
	"interbars" => ma > ma[1] * (1.0 + ma_margin)  ? color.aqua : ma < ma[1] * (1.0 - ma_margin) ? color.red : color.yellow
	"source" => (src - ma) / math.abs(src) > ma_margin ?
			 color.aqua :
			 (src - ma) / math.abs(src) < -1 * ma_margin ?
				 color.red :
				 color.yellow
	"none" => color.gray
	=>
		runtime.error("No such option found.")
		color.gray

// Plot unsmoothed current source (usually close price), this serves as a baseline to compare and better understand the moving average and ma_color (especially when "source" option is selected)
plot(src, color=color.purple, title="Source")
// Plot moving average
plot(ma, color=final_color, linewidth=2, title="Moving Average")
