dysonance / Indicators.jl

Financial market technical analysis & indicators in Julia
Other
216 stars 62 forks source link

John Ehlers Indicators - Cycle Analytics for Traders #13

Open flare9x opened 6 years ago

flare9x commented 6 years ago

Hey!

Sorry been neck deep in projects. Working on John Ehlers indicators from his latest book. Here are a few to get started:


# Supersmoother
# Equation 3-3
function supersmoother(x::Array{Float64}; n::Int64=10)::Array{Float64}
a = exp(-1.414*3.14159 / n)
b = 2 * a * cosd(1.414 * 180 / n)
c2 = b
c3 = -a * a
c1 = 1 - c2 - c3
@assert n<size(x,1) && n>0 "Argument n out of bounds."
Super = zeros(x)
 @inbounds for i = 3:length(x)
Super[i] = c1 * (x[i] + x[i-1]) / 2 + c2 * Super[i-1] + c3 * Super[i-2]
end
return Super
end

# Decycler 4-1
function decycler(x::Array{Float64}; n::Int64=60)::Array{Float64}
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
#Highpass filter cyclic components whose periods are shorter than “cutoff” bars
alpha1 = ((cosd(360 / n) + sind(360 / n) - 1)) / (cosd(360 / n))
Decycle = zeros(x)
 @inbounds for i in 2:length(x)
     Decycle[i] = (alpha1 / 2)*(x[i] + x[i-1]) + (1- alpha1)*Decycle[i-1]
end
return Decycle
end
# Decyler Oscillator 4-2
function Decycle_OSC(x::Array{Float64}; n1::Int64=30, n2::Int64=60)::Array{Float64}
        @assert n2<size(x,1) && n2>0 "Argument n out of bounds."
alpha1 = (cosd(.707*360 / n1) + sind(.707*360 / n1) - 1) / cosd(.707*360 / n1)
alpha2 = (cosd(.707*360 / n2) + sind(.707*360 / n2) - 1) / cosd(.707*360 / n2)
HP1 = zeros(x)
HP2 = zeros(x)
Decycle_OSC = zeros(x)
@inbounds for i in 3:length(x)
HP1[i] = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(x[i] - 2*x[i-1] + x[i-2]) + 2*(1 - alpha1)*HP1[i-1] - (1 - alpha1)* (1 - alpha1)*HP1[i-2]
HP2[i] = (1 - alpha2 / 2)*(1 - alpha2 / 2)*(x[i] - 2*x[i-1] + x[i-2]) + 2*(1 - alpha2)*HP2[i-1] - (1 - alpha2)*(1 - alpha2)*HP2[i-2]
end
Decycle_OSC .= HP2 .- HP1
return Decycle_OSC
end

When I get a chance I will take a look at the other implementations you made for the RS hurst. Ehlers has a hurst function which is also interesting and maybe of more value than the RS method.

More to come!

femtotrader commented 6 years ago

You should use triple backticks for code in Github issues https://help.github.com/articles/creating-and-highlighting-code-blocks/

flare9x commented 6 years ago

thanks!


# Band Pass Filter 5-1
function BandPassFilter(x::Array{Float64}; n::Int64=30, bandwidth::Float64=.3)::Array{Float64}
            @assert n<size(x,1) && n>0 "Argument n out of bounds."
alpha2 = (cosd(.25*bandwidth*360 / n) + sind(.25*bandwidth*360 / n) - 1) / cosd(.25*bandwidth*360 /n)
beta1 = cosd(360 / n);
gamma1 = 1 / cosd(360*bandwidth / n)
alpha1 = gamma1 - sqrt(gamma1*gamma1 - 1)
HP = zeros(x)
BP = zeros(x)
@inbounds for i in 3:length(x)
HP[i] = (1 + alpha2 / 2)*(x[i] - x[i-1]) + (1- alpha2)*HP[i-1]
BP[i] = .5*(1 - alpha1)*(HP[i] - HP[i-2]) + beta1*(1 + alpha1)*BP[i-1] - alpha1*BP[i-2]
end
# Signal
Signal = zeros(x)
Peak = zeros(x)
@inbounds for i in 2:length(BP)
Peak[i] = .991*Peak[i-1]
if abs(BP[i]) > Peak[i]
Peak[i] = abs(BP[i])
    Signal[i] = BP[i] / Peak[i]
else
        Signal[i] = BP[i] / Peak[i]
end
end

# Replace Nan to 0
@inbounds for i in 1:length(Signal)
    if isnan(Signal[i]) == 1
        Signal[i] = 0.0
    else
        Signal[i] = Signal[i]
    end
end

# Trigger
alpha2 = (cosd(1.5*bandwidth*360 / n) + sind(1.5*bandwidth*360 / n) - 1) / cosd(1.5*bandwidth*360 /n)
BP_Trigger = zeros(x)
i=1
@inbounds for i = 2:length(x)
BP_Trigger[i] = (1 + alpha2 / 2)*(Signal[i] - Signal[i-1]) +(1 -alpha2)*BP_Trigger[i-1]
end
return BP_Trigger
end
dysonance commented 6 years ago

@flare9x Thanks for the idea and for the good head start here. I'm familiar with some of John Ehlers's ideas — I implemented the MESA Adaptive Moving Average way back when I first started this project (function name mama), so you might want to take a look at that as well?

In the meantime I will try to make some time to get cracking on these other indicators as well. Looking forward to researching some of these, I think a lot of his ideas make a lot of sense so I'm excited to get back to reviewing some of his work and getting these implemented.

Cheers and thanks again!

flare9x commented 6 years ago

Your welcome! I am so far cross checking with Mr Ehlers implementation in tradestation and ensure obtaining the same result within Julia (essentially changing tradestation code to Julia). These are designed to have as minimal lag as possible so they are more responsive vs traditional. Look forward to building them out - I have not seen a full implementation yet so be good to have them all coded in this package :)

flare9x commented 6 years ago

Had trouble with Code Listing 5-2. Dominant Cycle Measured by Zero Crossings of the Band-Pass Filter - have all correct except for the DC portion of the calculation. Moved on to next and will revisit. Hurst below and same results as Ehlers implementation in his book Cycle analytics for traders.

# Ehlers Hurst 6-1
function hurst(x::Array{Float64}; n::Int64=30,)::Array{Float64}
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
        @assert iseven(n) "n must be an even number."
half_n = Int64(n/2)
a1 = exp(-1.414*3.14159 / 20)  # smoothes by 20 period same as equation 3-3- may wish to make this a argument in the function?
b1 = 2*a1*cosd(1.414*180 / 20) # smoothes by 20 period same as equation 3-3- may wish to make this a argument in the function?
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
# Find rolling maximum and minimum
HH = zeros(x)
LL = zeros(x)
N3 = zeros(x)
@inbounds for i = n:size(x,1)
                HH[i] = maximum(x[i-n+1:i])
                LL[i] = minimum(x[i-n+1:i])
                N3[i] = (HH[i] - LL[i]) / n
            end
# Rolling min and max half of n
HH = zeros(x)
LL = zeros(x)
N1 = zeros(x)
@inbounds for i = half_n:size(x,1)
                HH[i] = maximum(x[i-half_n+1:i])
                LL[i] = minimum(x[i-half_n+1:i])
                N1[i] = (HH[i] - LL[i]) / half_n
            end

# Set trailing close half of n
HH = [fill(0,half_n); x[1:length(x)-half_n]]
LL = [fill(0,half_n); x[1:length(x)-half_n]]
HH_out = zeros(x)
LL_out = zeros(x)
N2 = zeros(x)
@inbounds for i = half_n:size(x,1)
    HH_out[i] = maximum(HH[i-half_n+1:i])
    LL_out[i] = minimum(LL[i-half_n+1:i])
    N2[i] = (HH_out[i] - LL_out[i])/(half_n)
end

# Hurst
Dimen = zeros(x)
Hurst = zeros(x)
SmoothHurst = zeros(x)
@inbounds for i = 3:size(x,1)
if N1[i] > 0 && N2[i] > 0 && N3[i] > 0
    Dimen[i] = .5*((log(N1[i]+ N2[i]) - log(N3[i])) / log(2) + Dimen[i-1])
Hurst[i] = 2 - Dimen[i]
SmoothHurst[i] = c1*(Hurst[i] + Hurst[i-1]) / 2 + c2*SmoothHurst[i-1]+ c3*SmoothHurst[i-2];
end
end
return SmoothHurst
end
flare9x commented 6 years ago

Roofing filters from chapter 7:

# HP-LP Roofing Filter 7-1
function HpLpRoofingFilter(x::Array{Float64})::Array{Float64}
        @assert n<size(x,1) && n>0 "Argument n out of bounds."
# Highpass filter cyclic components whose periods are shorter than 48 bars
alpha1 = (cosd(360 / 48) + sind(360 / 48) - 1) / cosd(360 / 48)
HP = zeros(x)
@inbounds for i = 2:size(x,1)
    HP[i] = (1 - alpha1 / 2)*(x[i] - x[i-1]) + (1 - alpha1)*HP[i-1]
end
# Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / 10)  # may wish to make this an argument in function
b1 = 2*a1*cosd(1.414*180 / 10) # may wish to make this an argument in function
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
LP_HP_Filt = zeros(x)
@inbounds for i = 3:size(x,1)
LP_HP_Filt[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*LP_HP_Filt[i-1] + c3*LP_HP_Filt[i-2]
end
return LP_HP_Filt
end

# zero mean roofing filter 7-2
function ZeroMeanRoofingFilter_one(x::Array{Float64})::Array{Float64}
            @assert n<size(x,1) && n>0 "Argument n out of bounds."
# Highpass filter cyclic components whose periods are shorter than 48 bars
alpha1 = (cosd(360 / 48) + sind(360 / 48) - 1) /cosd(360 / 48)
HP = zeros(x)
@inbounds for i = 2:size(data1_c,1)
HP[i] = (1 - alpha1 / 2)*(x[i] - x[i-1]) +(1 - alpha1)*HP[i-1]
end
#Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / 10)
b1 = 2*a1*cosd(1.414*180 / 10)
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
Zero_Mean_Filt = zeros(x)
Zero_Mean_Filt2 = zeros(x)
@inbounds for i = 3:size(x,1)
Zero_Mean_Filt[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*Zero_Mean_Filt[i-1] + c3*Zero_Mean_Filt[i-2]
Zero_Mean_Filt2[i] = (1 - alpha1 / 2)*(Filt[i] - Filt[i-1]) + (1 - alpha1)*Filt2[i-1]
end
return Zero_Mean_Filt
end

function ZeroMeanRoofingFilter_two(x::Array{Float64})::Array{Float64}
            @assert n<size(x,1) && n>0 "Argument n out of bounds."
# Highpass filter cyclic components whose periods are shorter than 48 bars
alpha1 = (cosd(360 / 48) + sind(360 / 48) - 1) /cosd(360 / 48)
HP = zeros(x)
@inbounds for i = 2:size(data1_c,1)
HP[i] = (1 - alpha1 / 2)*(x[i] - x[i-1]) +(1 - alpha1)*HP[i-1]
end
#Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / 10)
b1 = 2*a1*cosd(1.414*180 / 10)
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
Zero_Mean_Filt = zeros(x)
Zero_Mean_Filt2 = zeros(x)
@inbounds for i = 3:size(x,1)
Zero_Mean_Filt[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*Zero_Mean_Filt[i-1] + c3*Zero_Mean_Filt[i-2]
Zero_Mean_Filt2[i] = (1 - alpha1 / 2)*(Zero_Mean_Filt[i] - Zero_Mean_Filt[i-1]) + (1 - alpha1)*Zero_Mean_Filt2[i-1]
end
return Zero_Mean_Filt2
end

# Roofing filter as indicator 7-3
function RoofingFilterIndicator(x::Array{Float64}; n1::Int64=40,n2::Int64=80)::Array{Float64}
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
LPPeriod = n1
HPPeriod = n2
#Highpass filter cyclic components whose periods are shorter than 48 bars
alpha1 = (cosd(.707*360 / HPPeriod) + sind(.707*360 /HPPeriod) - 1) / cosd(.707*360 / HPPeriod)
HP = zeros(x)
@inbounds for i = 3:length(x)
    HP[i] = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(x[i] - 2*x[i-1] + x[i-2]) + 2*(1 - alpha1)*HP[i-1] - (1 - alpha1)*(1 - alpha1)*HP[i-2]
end
#Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / LPPeriod)
b1 = 2*a1*cosd(1.414*180 / LPPeriod)
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
Roof_filt_Indicator = zeros(x)
@inbounds for i = 3:length(x)
Roof_filt_Indicator[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*Roof_filt_Indicator[i-1] + c3*Roof_filt_Indicator[i-2]
end
return Roof_filt_Indicator
end
flare9x commented 6 years ago

Modified stochastic:

# Modified Stochastic 7-4
function ModifiedStochastic(x::Array{Float64}; n::Int64=20)::Array{Float64}
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
#Highpass filter cyclic components whose periods are shorter than 48 bars
alpha1 = (cosd(.707*360 / 48) + sind(.707*360 / 48) - 1) /cosd(.707*360 / 48)
HP = zeros(x)
@inbounds for i = 3:size(x,1)
HP[i] = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(x[i] - 2*x[i-1]+ x[i-2]) + 2*(1 - alpha1)*HP[i-1] - (1 - alpha1)*(1 -alpha1)*HP[i-2]
end
#Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / 10)
b1 = 2*a1*cosd(1.414*180 / 10)
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
Filt = zeros(x)
@inbounds for i = 3:size(x,1)
Filt[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*Filt[i-1] + c3*Filt[i-2]
end
# Highest and lowest filt over n width
HighestC = zeros(x)
LowestC = zeros(x)
Stoc = zeros(x)
MyStochastic = zeros(x)
@inbounds for i = n:size(x,1)
    HighestC[i] = maximum(Filt[i-n+1:i])
    LowestC[i] = minimum(Filt[i-n+1:i])
    Stoc[i] = (Filt[i] - LowestC[i]) / (HighestC[i] - LowestC[i])
    MyStochastic[i] = c1*(Stoc[i] + Stoc[i-1]) / 2 + c2*MyStochastic[i-1] + c3*MyStochastic[i-2]
end
return MyStochastic
end
flare9x commented 6 years ago

Modified RSI

# Modified RSI 7-5
function ModifiedRSI(x::Array{Float64}; n::Int64=10)::Array{Float64}
    @assert n<size(x,1) && n>0 "Argument n out of bounds."
#Highpass filter cyclic components whose periods areshorter than 48 bars
alpha1 = (cosd(.707*360 / 48) + sind(.707*360 / 48) - 1) /cosd(.707*360 / 48)
HP = zeros(x)
@inbounds for i =3:size(x,1)
HP[i] = (1 - alpha1 / 2)*(1 - alpha1 / 2)*(x[i] - 2*x[i-1] +x[i-2]) + 2*(1 - alpha1)*HP[i-1] - (1 - alpha1)*(1 -alpha1)*HP[i-2]
end
#Smooth with a Super Smoother Filter from equation 3-3
a1 = exp(-1.414*3.14159 / 10)
b1 = 2*a1*cosd(1.414*180 / 10)
c2 = b1
c3 = -a1*a1
c1 = 1 - c2 - c3
Filt = zeros(x)
@inbounds for i = 3:size(x,1)
Filt[i] = c1*(HP[i] + HP[i-1]) / 2 + c2*Filt[i-1] + c3*Filt[i-2]
end
ClosesUp = zeros(x)
ClosesDn = zeros(x)
filtdiff = zeros(x)
posDiff= zeros(x)
negDiff= zeros(x)
# pos and neg diffs
@inbounds for i = 2:size(x,1)
# difference
filtdiff[i] = Filt[i] - Filt[i-1]
if filtdiff[i] > 0
    posDiff[i] = filtdiff[i]
elseif filtdiff[i] < 0
    negDiff[i] = abs(filtdiff[i])
end
end

# Running Sums of Filt
posSum = zeros(x)
negSum = zeros(x)
denom = zeros(x)
rsi= zeros(x)
@inbounds for i = n:size(x,1)
    posSum[i] = sum(posDiff[i-n+1:i])
    negSum[i] = sum(negDiff[i-n+1:i])
     denom[i] = posSum[i]+negSum[i]
end

# RSI
MyRSI = zeros(x)
@inbounds for i = 3:size(x,1)
if denom != 0 && denom[i-1] != 0
    MyRSI[i] = c1*(posSum[i] /denom[i] + posSum[i-1] / denom[i-1]) / 2 + c2*MyRSI[i-1] +c3*MyRSI[i-2]
end
end
return MyRSI
end
flare9x commented 6 years ago

I'l now continue to update the file here + TO DO list.

https://gist.github.com/flare9x/86fd4d5dc574eb2532355af7027e83a2#file-cycle_analytics_for_traders-jl

femtotrader commented 6 years ago

You might start your code blocks using:

```julia

so it will be highlight as Julia code

You should create a package for that (not just a gist)

For Julia 0.6 see https://docs.julialang.org/en/v0.6.4/manual/packages/ For Julia 0.7 and above see https://docs.julialang.org/en/latest/stdlib/Pkg/ (more precisely https://docs.julialang.org/en/latest/stdlib/Pkg/#Creating-your-own-packages-1 ) and https://discourse.julialang.org/t/ann-introducing-upgradathon-fridays/12047/3

That will be nice if you could from Tradestation both output (as csv) some input data and values of several indicators (also as CSV)

So those values could be used as unit tests.

flare9x commented 6 years ago

Thanks for the suggestion! Although not sure if its adding more than what can be done in this package. I am working on other things such as candlesticks etc.. I either do that or @dysonance doesn't mind me contributing to it here! Whatever works

Sinansi commented 4 years ago

Thanks for the suggestion! Although not sure if its adding more than what can be done in this package. I am working on other things such as candlesticks etc.. I either do that or @dysonance doesn't mind me contributing to it here! Whatever works

Please, keep up the good work. Thank you!

flare9x commented 4 years ago

Still looking for some help? Happy to collaborate!

dysonance commented 4 years ago

@flare9x Absolutely! Any effort you can give is welcome with open arms. Feel free to fork the repo and open a pull request with any improvements or additions if that works for you. Let me know if I can provide any guidance and/or what I can do to make the collaboration process easier.

kpa28-git commented 2 years ago

Hey I just saw this thread. I did some implementations of Ehler's work a while ago if anyone is interested:


"""
Exponential moving average (EMA)
"""
function ema(pₜ::AbstractVector{T}, α::Real=.07) where {T<:Real}
    @assert length(pₜ)>1 "pₜ must not be a singleton."
    cα = 1-α
    maₜ = zeros(length(pₜ))
    maₜ[1] = pₜ[1]
    @inbounds for t in 2:length(pₜ)
        maₜ[t] = α*pₜ[t] + cα*maₜ[t-1]
    end
    maₜ
end

"""
Ehlers Fisher Transform
Transforms the PDF of the input to approximately Gaussian

source: pp. 3, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
fisher(pₜ) = .5*log((1+pₜ)/(1-pₜ))

"""
Ehlers Generalized Linear DSP Filter.
Degree of two (`wₒ` like [wₒ₂, wₒ₁, 0] where wₒ₂>0, wₒ₁>0) is recommended by Ehlers for recursive filters.

All array inputs (including weight arrays `wᵢ`, `wₒ`) are in ascending time order.

Filter components are added instead of subtracted (as in the book) so that the filter weights in the indicator implementations exactly match Ehlers's code. Ehlers usually adds the components instead of subtracting when building filters even though in
the provided reference he subtracts.

source: pp. 11, Cycle Analytics for Traders by John Ehlers
"""
function dsp!(fₜ, pₜ, wᵢ, wₒ)
    τ = length(wᵢ)
    @assert τ == length(wₒ)

    for t in τ:length(pₜ)
        nonrec = sum(wᵢ .* pₜ[t-τ+1:t])
        rec = sum(wₒ .* fₜ[t-τ+1:t])
        fₜ[t] = nonrec + rec
    end
    fₜ
end

function dsp(pₜ, wᵢ, wₒ)
    dsp!(zeros(length(pₜ)), pₜ, wᵢ, wₒ)
end

"""
Generalized Smoothing / Non-Recursive Filter.
"""
dsp(pₜ, wᵢ) = slidedot(pₜ, wᵢ)

"""
Ehlers Symmetric Low Pass Finite Impluse Response (FIR) Filter.
This is a WMA with weights like `[1, 2, ..., 2, 1]`
source: pp. 33, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function symlowpass(pₜ, τ::Integer=4)
    w = repeat([2], τ)
    w[begin] = w[end] = 1
    dsp(pₜ, w) ./ sum(w)
end

"""
Simple Momentum
"""
function simplemom(pₜ, τ::Integer=2)
    momₜ = zeros(size(pₜ))
    for t in 1+τ:length(pₜ)
        momₜ[t] = 2pₜ[t] - pₜ[t-τ]
    end
    momₜ
end

"""
Ehlers Instantaneous Trendline Filter

source: pp. 24, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function itrend(pₜ, α::Real=.07)
    # Initialize first 7 bars to shorten convergence time:
    tₒ = 7
    trendₜ = zeros(length(pₜ))
    trendₜ[begin:tₒ] = symlowpass(pₜ[begin:tₒ], 3)

    α² = α^2
    wᵢ = [-(α-.75α²), .5α², α-.25α²]
    wₒ = [-(1-α)^2, 2(1-α), 0]
    dsp!(trendₜ, pₜ, wᵢ, wₒ)
end

"""
Ehlers Instantaneous Trendline Strategy

source: pp. 26, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function itrend_xostrat(pₜ, α::Real=.07, τ::Integer=2)
    trendₜ = itrend(pₜ, α)
    momₜ = simplemom(trendₜ, τ)
    buyₜ = crossover(momₜ, trendₜ)
    sellₜ = crossover(trendₜ, momₜ)
    buyₜ, sellₜ
end

"""
Ehlers Cyber Cycle Indicator

source: pp. 34, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function cybercycle(pₜ, α::Real=.07)
    # Initialize first 7 bars to shorten convergence time:
    tₒ = 7
    ccₜ = zeros(length(pₜ))
    ccₜ[begin:tₒ] = dsp(pₜ[begin:tₒ], [1, -2, 1]) ./ 4

    β = (1 - .5α)^2
    wᵢ = β * ([1, -2, 1])
    wₒ = [-(1-α)^2, 2(1-α), 0]
    dsp!(ccₜ, symlowpass(pₜ), wᵢ, wₒ)
end

"""
Ehlers Cyber Cycle Trading Strategy
Signals are generated from signal[t] and signal[t-1] crossovers

source: pp. 38, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function cybercycle_xostrat(pₜ, α::Real=.07, τ::Integer=9)
    ccₜ = cybercycle(pₜ, α)
    α₂ = 1 / (τ + 1)
    signalₜ = ema(ccₜ, α₂)
    signalₜ₋₁ = signalₜ[begin:end-1]
    signalₜ = signalₜ[begin+1:end]
    buyₜ = crossover(signalₜ₋₁, signalₜ)
    sellₜ = crossover(signalₜ, signalₜ₋₁)
    insert!(buyₜ, 1, false)
    insert!(sellₜ, 1, false)
    buyₜ, sellₜ
end

"""
Ehlers Center of Gravity (CG) Oscillator

source: pp. 49, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function cg(pₜ, τ::Integer=10)
    cgratio = dsp(pₜ, 1:τ) ./ slidesum(pₜ, τ)
    -cgratio .+ ((τ+1)/2)
end

"""
Ehlers Relative Vigor Index

source: pp. 58, Cybernetic Analysis for Stocks and Futures by John Ehlers
"""
function rvi(oₜ, hₜ, lₜ, cₜ, τ::Integer=10)
    co = slidesum(symlowpass(cₜ .- oₜ), τ)
    hl = slidesum(symlowpass(hₜ .- lₜ), τ)
    co ./ hl
end

function rvi(ohlcₜ::AbstractMatrix, τ::Integer=10)
    rvi(ohlcₜ[:, 1], ohlcₜ[:, 2], ohlcₜ[:, 3], ohlcₜ[:, 4], τ)
end

"""
Ehlers Relative Vigor Index - Adaptive
"""
function arvi()

end

"""
Ehlers Super Smoother Filter

source: pp. 33, Cycle Analytics for Traders by John Ehlers
"""
function supersmooth(pₜ, τ::Integer=10)
    α = ℯ^(-π√2/τ)
    α² = α^2
    β = 2α*cosd(180√2/τ)
    c = 1 - β + α²
    dsp(pₜ, .5c*([0, 1, 1]), [-α², β, 0])
end

"""
Ehlers Super Smoother Crossover Strat
τ₁ > τ₂: trend following
τ₁ < τ₂: mean reversion
"""
function supersmooth_xostrat(pₜ, τ₁::Integer=3, τ₂::Integer=7) 
    fastₜ = supersmooth(pₜ, τ₁)
    slowₜ = supersmooth(pₜ, τ₂)
    buyₜ = crossover(fastₜ, slowₜ)
    sellₜ = crossover(slowₜ, fastₜ)
    buyₜ, sellₜ
end

"""
Ehlers Decycler Oscillator Strat

source: pp. 41, Cycle Analytics for Traders by John Ehlers
"""
function decycler(pₜ, f::Integer=60)
    α = (cosd(360/f) + sind(360/f) - 1) / cosd(360/f)
    wᵢ = [.5α, .5α]
    wₒ = [1-α, 0]
    dsp(pₜ, wᵢ, wₒ)
end

"""
Ehlers Decycler Oscillator

source: pp. 44, Cycle Analytics for Traders by John Ehlers
"""
function decycler_osc(pₜ, f₁::Integer=30, f₂::Integer=60)
    function decyclercomp(pₜ, f::Integer)
        c = (√2/2)*360
        α = (cosd(c/f) + sind(c/f) - 1) / cosd(c/f)
        β = (1 - .5α)^2
        wᵢ = [β, -2β, β]
        wₒ = [-(1-α)^2, 2(1-α), 0]
        dsp(pₜ, wᵢ, wₒ)
    end
    decyclercomp(pₜ, f₂) .- decyclercomp(pₜ, f₁)
end

"""
Ehlers Roofing Filter

The Roofing Filter is a wide bandwidth bandpass filter,
combining a wide high pass filter with a wide low pass (supersmoother) filter.

For reference Ehlers's example sets the pass band periods from 10 bars to 48 bars.

The theory is the high pass filter removes irrelevant low frequency components
and the low pass filter removes aliasing noise from sampling at the wrong frequency.

source: pp. 78, Cycle Analytics for Traders by John Ehlers
"""
function roof(pₜ, τₗ::Integer=10, τₕ::Integer=48)
    α = (cosd(360/τₕ) + sind(360/τₕ) - 1) / cosd(360/τₕ)
    wᵢ₀ = 1 - α/2
    hpₜ = dsp(pₜ, wᵢ₀*([-1, 1]), [1-α, 0])
    supersmooth(hpₜ, τₗ)
end

"""
Alternate Roofing Filter used for Autocorrelation Periodogram

source: pp. 94-98, 103-109 Cycle Analytics for Traders by John Ehlers
"""
function roof_acp(pₜ, τₗ::Integer=10, τₕ::Integer=48)
    α = (cosd((.5√2)*360/τₕ) + sind((.5√2)*360/τₕ) - 1) / cosd((.5√2)*360/τₕ)
    wᵢ = (1 - α/2)^2 * ([1, -2, 1])
    wₒ = [-(1-α)^2, 2(1-α), 0]
    hpₜ = dsp(pₜ, wᵢ, wₒ)
    supersmooth(hpₜ, τₗ)
end