STD/Clutter-Filtered, Kaiser Window FIR Digital Filter [Loxx]
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © loxx

//@version=5
indicator("STD/Clutter-Filtered, Kaiser Window FIR Digital Filter [Loxx]",
     shorttitle = "STDCFKWFDF [Loxx]",
     overlay = true, 
     timeframe="", 
     timeframe_gaps = true)

import loxx/loxxexpandedsourcetypes/4

greencolor = #2DD204
redcolor = #D2042D 

clutterFilt(float src, float threshold)=>
    bool out = math.abs(ta.roc(src, 1)) > threshold
    out
    
stdFilter(float src, int len, float filter)=>
    float price = src
    float filtdev = filter * ta.stdev(src, len)
    price := math.abs(price - nz(price[1])) < filtdev ? nz(price[1]) : price
    price
    
//Bessel function, z-order hyperbolic
zorderHyperbolicBessel(float x)=>
    float besselAccuracy = 0.000001
    float bessel = 1.0
    float summ = 0
    float temp = 0
    float k = 2.0
    float factorial = 1.0
    temp := x / 2
    summ := temp * temp
    bessel += summ
    while summ > besselAccuracy 
        factorial := factorial * k
        temp *= x / 2
        summ := temp / factorial
        summ := summ * summ
        bessel += summ
        k += 1.0
    bessel

//Filter length estimations
filterOrder(float PassBandRipple, float StopBandAttenuation, float PassBandBars, float StopBandBars)=>
    float sbripple = 0
    float pbripple = 0
    float ripple = 0
    float attenuation = 0
    float bandwidth = 0
    float d = 0
    float n = 0
    float x = 0
    float alpha = 0
    
    float FilterLength = 0.
    PassBand = 1 / PassBandBars
    StopBand = 1 / StopBandBars
    bandwidth := PassBand + StopBand
    if bandwidth >= 0.5
        PassBand := 0.5 * PassBand / bandwidth
        StopBand := 0.5 * StopBand / bandwidth
    
    sbripple := math.pow(10.0, (-0.05 * StopBandAttenuation))
    pbripple := math.pow(10.0, (0.05 * PassBandRipple)) - 1.0
    ripple := math.min(sbripple, pbripple) 
    attenuation := -20 * math.log(ripple) / math.log(10) 
    if math.round(attenuation, 5) <= 21.0
        alpha := 0.0
        d := 0.9222
    
    if math.round(attenuation, 5) > 50.0
        alpha := 0.1102 * (attenuation - 8.7)
        d := (attenuation - 7.95) / 14.36
    
    if math.round(attenuation, 5) > 21.0 and math.round(attenuation, 5) <= 50
        alpha := (0.5842 * math.pow((attenuation - 21.0), 0.4)) + (0.07886 * (attenuation - 21.0))
        d := (attenuation - 7.95) / 14.36
    
    n := (d / StopBand) + 1.0
    x := math.round(n)
    if x % 2 < 1
        FilterLength := x
    else 
        FilterLength := x - 1
        
    [int(FilterLength), alpha, PassBand, StopBand]

Normalization(float PassBandRipple, float StopBandAttenuation, float PassBandBars, float StopBandBars)=>
    float filter = 0
    float Ioalfa = 0
    float temp = 0
    float norm = 0

    [FilterLength, alpha, PassBand, StopBand] = filterOrder(PassBandRipple, StopBandAttenuation, PassBandBars, StopBandBars)
    int M = int(FilterLength / 2)
    float[] filterCoeff = array.new<float>(FilterLength + 1, 0)
    float[] kaiserWindow = array.new<float>(M + 1, 0)
    
    //Window function 
    norm := M
    Ioalfa := zorderHyperbolicBessel(alpha)
    for i = 1 to M 
        temp := i / norm
        array.set(kaiserWindow, i, zorderHyperbolicBessel(alpha * math.sqrt(1 - (temp * temp))) / Ioalfa)

    //filter coefficients
    array.set(filterCoeff, 0, 2.0 * (PassBand + (0.5 * StopBand)))
    norm := array.get(filterCoeff, 0)
    temp := math.pi * array.get(filterCoeff, 0)
    for i = 1 to M 
        array.set(filterCoeff, i, math.sin(i * temp) * array.get(kaiserWindow, i) / (i * math.pi))
        norm := norm + (2 * array.get(filterCoeff, i)) 

    //casual conversion and normalization
    float[] NormCoef = array.new<float>(FilterLength + 1, 0)
    for i = M + 1 to FilterLength 
        array.set(filterCoeff, i, array.get(filterCoeff, i - M))
    for i = 0 to M - 1 
        array.set(filterCoeff, i, array.get(filterCoeff, FilterLength - i))
    array.set(filterCoeff, M, 2.0 * (PassBand + (0.5 * StopBand)))
    for i = 0 to FilterLength 
        array.set(NormCoef, i, array.get(filterCoeff, i) / norm)
    [NormCoef, FilterLength]

filterResponse(float src, float[] NormCoef, int per)=>
    float valueBuf = 0
    float temp = 0
    float temp1 = 0
    float Response = 0.0
    int i = 0
    int filterlength = 0
    while filterlength <= per
        valueBuf := nz(src[filterlength])
        Response := Response + valueBuf * array.get(NormCoef, filterlength)
        filterlength += 1
    Response

smthtype = input.string("Kaufman", "Heiken-Ashi Better Smoothing", options = ["AMA", "T3", "Kaufman"], group=  "Source Settings")
srcoption = input.string("HAB Median", "Source", group= "Source Settings", 
     options = 
     ["Close", "Open", "High", "Low", "Median", "Typical", "Weighted", "Average", "Average Median Body", "Trend Biased", "Trend Biased (Extreme)", 
     "HA Close", "HA Open", "HA High", "HA Low", "HA Median", "HA Typical", "HA Weighted", "HA Average", "HA Average Median Body", "HA Trend Biased", "HA Trend Biased (Extreme)",
     "HAB Close", "HAB Open", "HAB High", "HAB Low", "HAB Median", "HAB Typical", "HAB Weighted", "HAB Average", "HAB Average Median Body", "HAB Trend Biased", "HAB Trend Biased (Extreme)"])

//PassBand=1/PassBandBars -> relative range of filter pass band, possible range is from 0.0 to (0.5-StopBand)
//                           It is normalized and it's maximum value (0.5-StopBand) is 1/2                     
//StopBand=1/StopBandBars -> relative range of filter stop band, possible range is from 0.0 to (0.5-PassBand)
//                           It is normalized and it's maximum value (0.5-PassBand) is 1/2                 
// (PassBand + StopBand) < 0.5, Relative F_sampling=1, Abs F_sampling= Bar's frequency
// If (PassBand + StopBand) >= 0.5, they would be normalized
//	  Gain(dB)
//		|	     ..    <------pass band ripple (in dB)
//	 0dB|....  .  .                                     PassBand=Fpass;
//		|    ..    .                                    StopBand=Fstop-Fpass; width of stop band 
//		|	       	 .                                  Fstop<0.5*F_sample
//		|              .         
//		|               .     ..       <-----stop band attenuation (in dB) 
//		|                .  .    .
//	    |---------+-------+--------*-------+----------------------------------+------Frequency
//		0       Fpass   Fstop          0.5F_sample                          F_sample 
//		

PassBandBars = input.float(250.0, "Passband Bars", group = "Basic Settings")
StopBandBars = input.float(4, "Stopband Bars", group = "Basic Settings")
StopBandAttenuation = input.float(21.0, "Stopband Attenuation", group = "Basic Settings")
PassBandRipple = input.float(30.0, "Pass Band Ripple", group = "Basic Settings")

sth = input.float(0.1, "Clutter Filter Threshold", group = "Basic Settings", step = 0.001)

filterop = input.string("Both", "Filter Options", options = ["Price", "STDCFKWFDF", "Both", "None"], group=  "Filter Settings")
filter = input.float(0, "Filter Devaitions", minval = 0, group= "Filter Settings")
filterperiod = input.int(15, "Filter Period", minval = 0, group= "Filter Settings")

colorbars = input.bool(true, "Color bars?", group = "UI Options")
showSigs = input.bool(true, "Show signals?", group= "UI Options")
showdeadzones = input.bool(false, "Show dead zones?", group= "UI Options")

kfl=input.float(0.666, title="* Kaufman's Adaptive MA (KAMA) Only - Fast End", group = "Moving Average Inputs")
ksl=input.float(0.0645, title="* Kaufman's Adaptive MA (KAMA) Only - Slow End", group = "Moving Average Inputs")
amafl = input.int(2, title="* Adaptive Moving Average (AMA) Only - Fast", group = "Moving Average Inputs")
amasl = input.int(30, title="* Adaptive Moving Average (AMA) Only - Slow", group = "Moving Average Inputs")

haclose = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, close)
haopen = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, open)
hahigh = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, high)
halow = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, low)
hamedian = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, hl2)
hatypical = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, hlc3)
haweighted = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, hlcc4)
haaverage = request.security(ticker.heikinashi(syminfo.tickerid), timeframe.period, ohlc4)

float src = switch srcoption
	"Close" => loxxexpandedsourcetypes.rclose()
	"Open" => loxxexpandedsourcetypes.ropen()
	"High" => loxxexpandedsourcetypes.rhigh()
	"Low" => loxxexpandedsourcetypes.rlow()
	"Median" => loxxexpandedsourcetypes.rmedian()
	"Typical" => loxxexpandedsourcetypes.rtypical()
	"Weighted" => loxxexpandedsourcetypes.rweighted()
	"Average" => loxxexpandedsourcetypes.raverage()
    "Average Median Body" => loxxexpandedsourcetypes.ravemedbody()
	"Trend Biased" => loxxexpandedsourcetypes.rtrendb()
	"Trend Biased (Extreme)" => loxxexpandedsourcetypes.rtrendbext()
	"HA Close" => loxxexpandedsourcetypes.haclose(haclose)
	"HA Open" => loxxexpandedsourcetypes.haopen(haopen)
	"HA High" => loxxexpandedsourcetypes.hahigh(hahigh)
	"HA Low" => loxxexpandedsourcetypes.halow(halow)
	"HA Median" => loxxexpandedsourcetypes.hamedian(hamedian)
	"HA Typical" => loxxexpandedsourcetypes.hatypical(hatypical)
	"HA Weighted" => loxxexpandedsourcetypes.haweighted(haweighted)
	"HA Average" => loxxexpandedsourcetypes.haaverage(haaverage)
    "HA Average Median Body" => loxxexpandedsourcetypes.haavemedbody(haclose, haopen)
	"HA Trend Biased" => loxxexpandedsourcetypes.hatrendb(haclose, haopen, hahigh, halow)
	"HA Trend Biased (Extreme)" => loxxexpandedsourcetypes.hatrendbext(haclose, haopen, hahigh, halow)
	"HAB Close" => loxxexpandedsourcetypes.habclose(smthtype, amafl, amasl, kfl, ksl)
	"HAB Open" => loxxexpandedsourcetypes.habopen(smthtype, amafl, amasl, kfl, ksl)
	"HAB High" => loxxexpandedsourcetypes.habhigh(smthtype, amafl, amasl, kfl, ksl)
	"HAB Low" => loxxexpandedsourcetypes.hablow(smthtype, amafl, amasl, kfl, ksl)
	"HAB Median" => loxxexpandedsourcetypes.habmedian(smthtype, amafl, amasl, kfl, ksl)
	"HAB Typical" => loxxexpandedsourcetypes.habtypical(smthtype, amafl, amasl, kfl, ksl)
	"HAB Weighted" => loxxexpandedsourcetypes.habweighted(smthtype, amafl, amasl, kfl, ksl)
	"HAB Average" => loxxexpandedsourcetypes.habaverage(smthtype, amafl, amasl, kfl, ksl)
    "HAB Average Median Body" => loxxexpandedsourcetypes.habavemedbody(smthtype, amafl, amasl, kfl, ksl)
	"HAB Trend Biased" => loxxexpandedsourcetypes.habtrendb(smthtype, amafl, amasl, kfl, ksl)
	"HAB Trend Biased (Extreme)" => loxxexpandedsourcetypes.habtrendbext(smthtype, amafl, amasl, kfl, ksl)
	=> haclose
	
src := filterop == "Both" or filterop == "Price" and filter > 0 ? stdFilter(src, filterperiod, filter) : src

[NormCoef, _filterLength] = Normalization(PassBandRipple, StopBandAttenuation, PassBandBars, StopBandBars)
out = filterResponse(src, NormCoef, _filterLength)

out := filterop == "Both" or filterop == "STDCFKWFDF" and filter > 0 ? stdFilter(out, filterperiod, filter) : out
sig = nz(out[1])

filtTrend = clutterFilt(out, sth)
state = filtTrend ? (out > sig ? 1 : out < sig ? -1 : 0) : 0

pregoLong = state == 1
pregoShort =state == -1

contsw = 0
contsw := nz(contsw[1])
contsw := pregoLong ? 1 : pregoShort ? -1 : nz(contsw[1])

goLong = pregoLong and nz(contsw[1]) == -1
goShort = pregoShort and nz(contsw[1]) == 1 

color colorout = na
colorout := filtTrend ? (state == 1 ? greencolor : state == -1 ? redcolor : showdeadzones ? color.gray : colorout[1]) : showdeadzones ? color.gray : colorout[1]

plot(out, "STDCFKWFDF", color = colorout, linewidth = 3)

barcolor(colorbars ? colorout : na)

plotshape(showSigs and goLong, title = "Long", color = color.yellow, textcolor = color.yellow, text = "L", style = shape.triangleup, location = location.belowbar, size = size.tiny)
plotshape(showSigs and goShort, title = "Short", color = color.fuchsia, textcolor = color.fuchsia, text = "S", style = shape.triangledown, location = location.abovebar, size = size.tiny)

alertcondition(goLong, title = "Long", message = "STD/Clutter-Filtered, Kaiser Window FIR Digital Filter [Loxx]: Long\nSymbol: {{ticker}}\nPrice: {{close}}")
alertcondition(goShort, title = "Short", message = "STD/Clutter-Filtered, Kaiser Window FIR Digital Filter [Loxx]: Short\nSymbol: {{ticker}}\nPrice: {{close}}")

