This is part of a series on technical analysis indicators in F#, based on the multi-language TA-Lib.

Quick disclaimer : some of these indicators are not verified.

Ehlers

Most of these studies are recursive, and thus the first 50 values or so should be discarded to saty out of the “construction phase” of the series.

namespace Trading.Studies

//
// John Ehlers - mostly from Cybernetic Analysis for stocks and futures (2004)
//

module Ehlers =

  //helpers

  let CURRENT_BAR_6 = 5

  let arrayGet (data:float[]) i =
    if i >= 0 then data.[i] else 0.0

  let quadrature (data:float[]) i =
    let a = arrayGet data i
    let b = arrayGet data (i-2)
    let c = arrayGet data (i-4)
    let d = arrayGet data (i-6)
    0.0962*a + 0.5769*b - 0.5769*c - 0.0962*d

  let inPhase (data:float[]) i = arrayGet data (i-3)

  //Trigger line used for any study
  let triggerLookback = 1

  [<TradingStudy;
    Group("Misc");
    Title("Trigger Line");
    Description("Returns a trigger line to generate cross-over signals");
    Overlay;
  >]
  let trigger (data:float[]) =
    Array.init (Array.length data - 1) (fun i -> data.[i+1])

  //Fisher transform
  let fisherTLookback period =
    Osc.stochFastKLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Fisher Transform");
    Description("Returns the Ehlers Fisher transform of a series - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let fisherT period (h:float[]) l c =
    Study.checkPositiveIntMin1 period
    Study.checkHighLowClose h l c
    let lookbackTotal = fisherTLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let hh = Stat.max period h
      let ll = Stat.min period l

      let mutable normalizedRatio = 0.0
      let mutable fisher = 0.0
      for i in startIdx .. endIdx do
        let outIdx = i - startIdx
        let fastKIdx = outIdx
        let fastK =
          let denom = hh.[fastKIdx] - ll.[fastKIdx]
          if denom <> 0.0 then (c.[i] - ll.[fastKIdx]) / denom else 0.0
        normalizedRatio <- (fastK-0.5) + 0.5*normalizedRatio |> min 0.9999 |> max (-0.9999)
        fisher <- 0.25*log((1.0+normalizedRatio)/(1.0-normalizedRatio)) + 0.5*fisher

        out.[outIdx] <- fisher       

      out
    )

  //===============================
  //
  // Instantaneous trendline
  //
  //===============================

  let itrendLookback = MA.trimaLookback 3

  [<TradingStudy;
    Group("Smoothing");
    Title("Ehlers Instantaneous Trendline");
    Description("Returns the Ehlers instantaneous trendline - from Cybernetic Analysis for stocks and futures (2004)");
    Overlay;
  >]
  let itrend alpha (data:float[]) =
    Study.checkPositiveReal alpha
    let lookbackTotal = itrendLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let trimaEndIdx = min endIdx CURRENT_BAR_6
      for i in startIdx .. trimaEndIdx do
        let outIdx = i - startIdx
        out.[outIdx] <- 0.25 * (data.[i] + 2.0*data.[i-1] + data.[i-2])

      if endIdx > trimaEndIdx then
        let a = alpha * alpha
        let b = 1.0 - alpha
        let c = alpha - (0.25 * a)
        let d = 0.5 * a
        let e = alpha - (0.75 * a)
        let f = 2.0 * b
        let g = b * b

        for i in trimaEndIdx+1 .. endIdx do
          let outIdx = i - startIdx
          out.[outIdx] <- c*data.[i] + d*data.[i-1] - e*data.[i-2] + f*out.[outIdx-1] - g*out.[outIdx-2]

      out
    )

  //===============================
  //
  // Cyber cycle
  //
  //===============================

  let ccycleLookback = 2

  let ccycleKnownSmooth startIdx endIdx alpha (data:float[]) (smooth:float[]) =
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let trimaEndIdx = min endIdx CURRENT_BAR_6
      for i in startIdx .. trimaEndIdx do
        let outIdx = i - startIdx
        out.[outIdx] <- 0.25 * (data.[i] - 2.0*data.[i-1] + data.[i-2])

      if endIdx > trimaEndIdx then
        let a = 1.0 - alpha
        let b = 1.0 - 0.5 * alpha
        let c = b * b
        let d = 2.0 * a
        let e = a * a

        for i in trimaEndIdx+1 .. endIdx do
          let outIdx = i - startIdx
          //smooth = 4-bar triangular mov avg
          //first output values = 3-bar triangular mov avg
          //--> smooth has a one-bar lag vs output values
          let smoothIdx = outIdx - 1
          let x = smooth.[smoothIdx] - 2.0*smooth.[smoothIdx-1] + smooth.[smoothIdx-2]
          out.[outIdx] <- c*x + d*out.[outIdx-1] - e*out.[outIdx-2]
      out
    )

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Cyber Cycle");
    Description("Returns the Ehlers cyber cycle - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let ccycle alpha (data:float[]) =
    Study.checkPositiveReal alpha
    let lookbackTotal = ccycleLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      ccycleKnownSmooth startIdx endIdx alpha data (MA.trima 4 data)
    )

  //===============================
  //
  //Center of gravity
  //
  //===============================

  let cgLookback period = period - 1

  //used for Centrer of gravity && adaptive ceter of gravity
  let cgAux wsum sum delta =
    -(wsum / sum) + delta

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Center of Gravity");
    Description("Returns the Ehlers center of gravity - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let cg period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = cgLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let mutable wsum = 0.0
      let mutable sum = 0.0

      let n = float period
      let delta = (n + 1.0) / 2.0

      let mutable w = n
      for i in startIdx - lookbackTotal .. startIdx do
        wsum <- wsum + (w * data.[i])
        sum <- sum + data.[i]
        w <- w - 1.0

      out.[0] <- cgAux wsum sum delta

      for i in startIdx+1 .. endIdx do
        let outIdx = i - startIdx
        sum <- sum + data.[i] - data.[i-period]
        wsum <- wsum + sum - (n * data.[i-period])
        out.[outIdx] <- cgAux wsum sum delta

      out
    )

  //===============================
  //
  //Relative vigor index
  //
  //===============================

  let rviLookback period =
    MA.trimaLookback 4 + Math.sumLookback period

  let rviAux x y =
    Array.map2 (-) x y |> MA.trima 4 

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Relative Vigor Index");
    Description("Returns the Ehlers relative vigor index - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let rvi period o h l (c:float[]) =
    Study.checkPositiveIntMin1 period
    Study.checkOpenHighLowClose o h l c
    let lookbackTotal = rviLookback period
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let co = rviAux c o |> Math.sum period
      let hl = rviAux h l |> Math.sum period
      Array.map2 (fun co hl -> if hl <> 0.0 then co / hl else 0.0) co hl
    )

  //===============================
  //
  // Stochastization & Fisherization
  //
  //===============================

  let stochFastKLookback period = Osc.stochFastKLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Fast K");
    Description("Returns the Ehlers fast K of a series - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let stochFastK period data =
    Osc.stochFastK period data data data

  let stochFastLookback fastKPeriod slowKPeriod =
    Osc.stochFastLookback fastKPeriod MA.Weighted slowKPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Fast Stochastics");
    Description("Returns the Ehlers fast K and slow K of a series - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let stochFast fastKPeriod slowKPeriod data =
    Osc.stochFast fastKPeriod MA.Weighted slowKPeriod data data data

  let stochSlowLookback fastKPeriod slowKPeriod slowDPeriod =
    Osc.stochSlowLookback fastKPeriod MA.Weighted slowKPeriod MA.Weighted slowDPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Slow Stochastics");
    Description("Returns the Ehlers fast K, slow K and slow D of a series - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let stochSlow fastKPeriod slowKPeriod slowDPeriod data =
    Osc.stochSlow fastKPeriod MA.Weighted slowKPeriod MA.Weighted slowDPeriod data data data

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Fisher Transform");
    Description("Same as fisherT : returns the Ehlers Fisher transform - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let fisher data = Signal.fisherT data  

  let fisherLookback = Signal.fisherTLookback

  //===============================
  //
  //Dominant Cycle Period
  //
  //===============================

  let dcpSmoothLookback = MA.trimaLookback 4

  let dcpQ1Lookback = 6

  let dcpDPLookback = 1 

  let dcpMedianDeltaLookback = 4     

  let dcpLookback =
    ccycleLookback + dcpQ1Lookback + dcpDPLookback + dcpMedianDeltaLookback

  let dcpFullAux alpha (ccycle:float[]) =
    let startIdx = 0
    let endIdx = ccycle.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let mutable prevQ1 = 0.0
      let mutable prevI1 = 0.0
      let mutable q1 = 0.0
      let mutable i1 = 0.0
      let mutable deltaPhase = 0.0
      let mutable medianDelta = Array.zeroCreate 5
      let mutable dc = 0.0
      let mutable instPeriod = 0.0
      let mutable period = 0.0

      for i in 0 .. ccycle.Length - 1 do
        prevQ1 <- q1
        prevI1 <- i1
        q1 <- quadrature ccycle i * (0.5 + 0.08*instPeriod)
        i1 <- inPhase ccycle i
        deltaPhase <-
          if (q1 <> 0.0 && prevQ1 <> 0.0) then
            let re = i1/q1 - prevI1/prevQ1
            let im = 1.0 + (i1*prevI1)/(q1*prevQ1)
            re/im
          else
            0.0
        medianDelta.[Study.circularIndex i medianDelta] <- deltaPhase
        let median = Study.median medianDelta |> max 0.1 |> min 1.1
        dc <- if median = 0.0 then 15.0 else 6.28318/median + 0.5
        instPeriod <- 0.33*dc + 0.67*instPeriod
        period <- 0.15*instPeriod + 0.85*period

        out.[i] <- period

      out
    )

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Dominant Cycle Period");
    Description("Returns the Ehlers dominant cycle period - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let dcp alpha (data:float[]) =
    (ccycle alpha data |> dcpFullAux alpha).[dcpLookback-ccycleLookback..]

  //===============================
  //
  //Adaptive Cyber Cycle
  //
  //===============================

  let accycleLookback = dcpLookback

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Adaptive Cyber Cycle");
    Description("Returns the Ehlers adaptive cyber cycle - from Cybernetic Analysis for stocks and futures (2004) -
    Adaptive indicators rely on the Ehlers dominant cycle period rather than on a fixed period");
  >]
  let accycle alpha (data:float[]) =
    Study.checkPositiveReal alpha
    let lookbackTotal = accycleLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      //smooth is also computed by ccycle below
      //but we need it to compute the adaptive values.

      //Besides since the formula is recursive, we also need
      //the values which are used to seed the first dominant cycle period
      //value, but which are not returned by the indicator.
      //Hence, we need to reuse the whole dominant cycle period computation.

      let smooth = MA.trima 4 data
      let ccycle = ccycleKnownSmooth ccycleLookback endIdx alpha data smooth
      let dcpFull = dcpFullAux alpha ccycle

      //adaptive output values
      let mutable twoAgo = (data.[4] - 2.0*data.[3] + data.[2]) / 4.0
      let mutable oneAgo = (data.[5] - 2.0*data.[4] + data.[3]) / 4.0
      let mutable current = 0.0

      let ccycleStartIdx = startIdx - ccycleLookback

      //The cyber cycle formula gets recursive after
      //at its 5th value (ccycle_idx=6) - or currentBar=7.
      for i in 4 .. ccycle.Length - 1 do
        //we create a coefficient based on the full dominant cycle period
        let beta = 2.0 / (dcpFull.[i] + 1.0)
        let a = 1.0 - beta
        let b = 1.0 - 0.5 * beta
        let c = b * b
        let d = 2.0 * a
        let e = a * a

        //smooth returns one bar less than the cyber cycle
        let smoothIdx = i - 1
        let x = smooth.[smoothIdx] - 2.0*smooth.[smoothIdx-1] + smooth.[smoothIdx-2]
        current <- c*x + d*oneAgo - e*twoAgo
        twoAgo <- oneAgo
        oneAgo <- current

        if i >= ccycleStartIdx then
          out.[i - ccycleStartIdx] <- current

      out
    )

  //===============================
  //
  //Adaptive Center of Gravity
  //
  //===============================

  let acgLookback = dcpLookback

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Adaptive Center of Gravity");
    Description("Returns the Ehlers adaptive center of gravity - from Cybernetic Analysis for stocks and futures (2004) -
    Adaptive indicators rely on the Ehlers dominant cycle period rather than on a fixed period");
  >]
  let acg alpha (data:float[]) =
    let endIdx = data.Length - 1
    dcp alpha data |> Array.mapi (fun dcpIdx period ->
      //the integer portion of the half dominant cycle period is used
      let intPart = period / 2.0 |> floor
      let intPeriod = int intPart
      let delta = (intPart + 1.0) / 2.0

      let mutable wsum = 0.0
      let mutable sum = 0.0      

      let dataIdx = dcpIdx + dcpLookback

      for i in dataIdx .. -1 .. max 0 (dataIdx - intPeriod + 1) do
        let w = dataIdx - i + 1 |> float
        wsum <- wsum + (w * data.[i])
        sum <- sum + data.[i]

      cgAux wsum sum delta
    )

  //===============================
  //
  //Adaptive Relative vigor index
  //
  //===============================

  let arviDcpAverageLookback =
    //in fact, it's not a real weighted moving average, but the formula
    //is pretty resembling
    MA.wmaLookback 5 

  let arviLookback =
    dcpLookback + arviDcpAverageLookback

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Adaptive Relative Vigor Index");
    Description("Returns the Ehlers adaptive relative vigor index - from Cybernetic Analysis for stocks and futures (2004) -
    Adaptive indicators rely on the Ehlers dominant cycle period rather than on a fixed period");
  >]
  let arvi alpha data o h l (c:float[]) =
    Study.checkPositiveReal alpha
    Study.checkOpenHighLowClose o h l c
    let lookbackTotal = arviLookback
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let co = rviAux c o
      let hl = rviAux h l

      //Dominant cycle period is smoothed contrary to other adaptive indicators
      //the smoothing looks like a weighted ma, but the denominator is larger
      //than the sum of the weights so that the study takes into account the
      //integer portion of the half dominant cycle period.
      //
      //The values taken into account do not follow each other in the average :
      //we have 4 values spaced over a 5-bar interval.
      //
      //More weight is given to the most recent bar.
      let smoothedPeriod =
        let dcp = dcp alpha data
        Array.init (dcp.Length - 4) (fun i ->
          (dcp.[i] + 2.0*dcp.[i+1] + 3.0*dcp.[i+3] + 4.0*dcp.[i+4]) / 20.0 |> int
        )

      let mutable v1 = 0.0
      let mutable v2 = 0.0
      for i in startIdx .. endIdx do
        let valueIdx = i - dcpSmoothLookback
        let periodIdx = i - startIdx
        let outIdx = periodIdx
        v1 <- 0.0
        v2 <- 0.0
        for j in valueIdx .. -1 .. max 0 (valueIdx - smoothedPeriod.[periodIdx]) do
          v1 <- v1 + co.[j]
          v2 <- v2 + hl.[j]
        out.[outIdx] <- if v2 <> 0.0 then v1 / v2 else 0.0

      out

    )

  //===============================
  //
  // Sinewave indicator
  //
  //===============================

  let sinewaveLookback = dcpLookback

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Sinewave");
    Description("Returns the Ehlers sinewave - from Cybernetic Analysis for stocks and futures (2004)");
    OutputSeriesNames("sine, lead");
  >]
  let sinewave alpha (data:float[]) =
    Study.checkPositiveReal alpha
    let lookbackTotal = sinewaveLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let sine = Array.zeroCreate outLen
      let sineLead = Array.zeroCreate outLen

      let cc = ccycle alpha data
      let dcp = dcp alpha data

      let mutable re = 0.0
      let mutable im = 0.0
      let offset = sinewaveLookback - ccycleLookback
      let mutable dcPhase = 0.0
      for periodIdx in 0 .. dcp.Length - 1 do
        let dcperiod = floor dcp.[periodIdx]
        re <- 0.0
        im <- 0.0

        let ccIdx = periodIdx + offset
        for i in ccIdx .. -1 .. max 0 (ccIdx - int dcperiod + 1) do
          let theta =
            let x = ccIdx - i |> float
            360.0 * x / dcperiod |> Study.degToRad
          re <- re + cc.[i] * sin theta
          im <- im + cc.[i] * cos theta

        if abs im > 0.001 then dcPhase <- atan(re/im) |> Study.radToDeg
        elif re > 0.0 then dcPhase <- 90.0
        else dcPhase <- -90.0

        dcPhase <- dcPhase + 90.0 

        if im < 0.0 then dcPhase <- dcPhase + 180.0
        if dcPhase > 315.0 then dcPhase <- dcPhase - 360.0

        let outIdx = periodIdx
        sine.[outIdx] <- dcPhase |> Study.degToRad |> sin
        sineLead.[outIdx] <- dcPhase + 45.0 |> Study.degToRad |> sin

      sine, sineLead
    )    

  //===============================
  //
  //Adaptive momentum
  //
  //===============================

  let amomLookback = dcpLookback

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Adaptive Momentum");
    Description("Returns the Ehlers adaptive momentum - from Cybernetic Analysis for stocks and futures (2004) -
    Adaptive indicators rely on the Ehlers dominant cycle period rather than on a fixed period");
  >]
  let amom cutoff alpha (data:float[]) =
    Study.checkPositiveReal alpha
    let lookbackTotal = amomLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      //smooth is also computed by ccycle below
      //but we need it to compute the adaptive values... argh !

      //Besides since the formula is recursive, we also need
      //the values which are used to seed the first dominant cycle period
      //value, but which are not returned by the indicator.
      //Hence, we need to reuse the whole dominant cycle period computation.

      let smooth = MA.trima 4 data
      let ccycle = ccycleKnownSmooth ccycleLookback endIdx alpha data smooth
      let dcpFull = dcpFullAux alpha ccycle

      //adaptive mom constants
      let a = exp(-System.Math.PI / cutoff)
      let b = 2.0 * a * cos(1.7318 * 180.0 / cutoff |> Study.degToRad)
      let c = a * a
      let coef2 = b + c
      let coef3 = -(c + b*c)
      let coef4 = c * c
      let coef1 = 1.0 - coef2 - coef3 - coef4

      let amomStartIdx = startIdx - ccycleLookback

      let computeValue1 i =
        let dataIdx = i + ccycleLookback
        let dataIdxLag = dataIdx - (int dcpFull.[i] - 1)
        if dataIdxLag >= 0 then
          data.[dataIdx] - data.[dataIdxLag]
        else
          0.0

      //adaptive mom
      let mutable value1 = computeValue1 0
      //adaptive output values
      let mutable threeAgo = 0.0
      let mutable twoAgo = 0.0
      let mutable oneAgo = 0.0
      let mutable current = value1

      for i in 1 .. ccycle.Length - 1 do
        //adaptive momentum
        let value1 = computeValue1 i

        //Algo says currentBar < 4
        //However since the first ccycle bar is at currentBar = 3
        //there only needs one such definition, which is done prior to looping
        current <- coef1*value1 + coef2*oneAgo + coef3*twoAgo + coef4*threeAgo

        threeAgo <- twoAgo
        twoAgo <- oneAgo
        oneAgo <- current

        //The cyber cycle formula gets recursive after
        //at its 5th value (ccycle_idx=6) - or currentBar=7.
        if i >= amomStartIdx then
          out.[i - amomStartIdx] <- current

      out
    )

  //===============================
  //
  // Two-Pole Butterworth filter
  //
  //===============================

  let twoPbfLookback = 0

  let twoPoleAux coef1 coef2 coef3 (data:float[]) =
    let out = Array.zeroCreate data.Length
    out.[0] <- data.[0]

    //there could be only one value in the input data.
    if data.Length > 1 then
      out.[1] <- data.[1]

    if data.Length > 2 then
      let smooth = MA.trima 3 data
      let recursionStartIdx = MA.trimaLookback 3
      let endIdx = data.Length - 1
      for i in recursionStartIdx .. endIdx do
        //The tradestation formula is inconsistent with the e-signal version
        //the latter seems more in-line with previous formulas, and we therefore use it
        out.[i] <- coef1*smooth.[i-recursionStartIdx] + coef2*out.[i-1] + coef3*out.[i-2]
    out

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Two-Pole Butterworth Filter");
    Description("Returns the Ehlers two-pole Butterworth filter - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let twoPbf period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = twoPbfLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //constants
      let period = float period
      let a = exp(-1.414*System.Math.PI / period)
      let b = 2.0 * a * cos(1.414 * 180.0 / period |> Study.degToRad)
      let coef2 = b
      let coef3 = (-a) * a
      let coef1 = (1.0 - b + a*a) / 4.0

      twoPoleAux coef1 coef2 coef3 data
    )

  //===============================
  //
  // Three-Pole Butterworth filter
  //
  //===============================

  let threePbfLookback = 0

  let threePoleAux coef1 coef2 coef3 coef4 (data:float[]) =
    let out = Array.zeroCreate data.Length
    out.[0] <- data.[0]

    //there could be only one value in the input data.
    if data.Length > 1 then
      out.[1] <- data.[1]

    if data.Length > 2 then
      out.[2] <- data.[2]

    if data.Length > 2 then
      let smooth = MA.trima 4 data
      let recursionStartIdx = MA.trimaLookback 4
      let endIdx = data.Length - 1
      for i in recursionStartIdx .. endIdx do
        out.[i] <- coef1*smooth.[i-recursionStartIdx] + coef2*out.[i-1] + coef3*out.[i-2] + coef4*out.[i-3]

    out

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Three-Pole Butterworth Filter");
    Description("Returns the Ehlers three-pole Butterworth filter - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let threePbf period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = threePbfLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //constants
      let period = float period
      let a = exp(-1.414*System.Math.PI / period)
      let b = 2.0 * a * cos(1.414 * 180.0 / period |> Study.degToRad)
      let c = a * a
      let coef2 = b + c
      let coef3 = -(c + b*c)
      let coef4 = c * c
      let coef1 = (1.0 - b + c) * (1.0 - c) / 8.0

      threePoleAux coef1 coef2 coef3 coef4 data
    )

  //===============================
  //
  // Two-Pole Super Smoother
  //
  //===============================

  let twoPssLookback = 0

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Two-Pole Super Smoother");
    Description("Returns the Ehlers two-pole super smoother - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let twoPss period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = twoPssLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //constants
      let period = float period
      let a = exp(-1.414*System.Math.PI / period)
      let b = 2.0 * a * cos(1.414 * 180.0 / period |> Study.degToRad)
      let coef2 = b
      let coef3 = (-a) * a
      let coef1 = (1.0 - b + a*a) / 4.0

      twoPoleAux coef1 coef2 coef3 data
    )

  //===============================
  //
  // Three-Pole Super Smoother
  //
  //===============================

  let threePssLookback = 0

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Three-Pole Super Smoother");
    Description("Returns the Ehlers three-pole super smoother - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let threePss period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = threePssLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //constants
      let period = float period
      let a = exp(-1.414*System.Math.PI / period)
      let b = 2.0 * a * cos(1.414 * 180.0 / period |> Study.degToRad)
      let c = a * a
      let coef2 = b + c
      let coef3 = -(c + b*c)
      let coef4 = c * c
      let coef1 = (1.0 - b + c) * (1.0 - c) / 8.0

      threePoleAux coef1 coef2 coef3 coef4 data
    )

  //===============================
  //
  // laguerre Filter
  //
  //===============================

  let laguerreLookback = 0

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Laguerre Filter");
    Description("Returns the Ehlers Laguerre filter - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let laguerre gamma (data:float[]) =
    Study.checkPositiveReal gamma
    let lookbackTotal = laguerreLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let alpha = 1.0 - gamma
      let beta = -gamma

      let mutable prevL0 = 0.0
      let mutable prevL1 = 0.0
      let mutable prevL2 = 0.0
      let mutable prevL3 = 0.0

      let mutable L0 = 0.0
      let mutable L1 = 0.0
      let mutable L2 = 0.0
      let mutable L3 = 0.0

      for i in 0 .. endIdx do
        L0 <- alpha*data.[i] + gamma*L0
        L1 <- beta*L0 +  prevL0 + gamma*L1
        L1 <- beta*L1 +  prevL1 + gamma*L2
        L1 <- beta*L2 +  prevL2 + gamma*L3

        prevL0 <- L0
        prevL1 <- L1
        prevL2 <- L2
        prevL3 <- L3

        if i >= startIdx then
          out.[i-startIdx] <- (L0 + 2.0*L1 + 2.0*L2 + L3) / 6.0

      out
    )

  //===============================
  //
  // Laguerre RSI
  //
  //===============================

  let laguerreRsiLookback = 0

  let laguerreRsiAux x y (cu, cd) =
    let delta = x - y
    if delta > 0.0 then (cu + delta, cd) else (cu, cd - delta)

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Laguerre RSI");
    Description("Returns the Ehlers Laguerre RSI (relative strength index) - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let laguerreRsi gamma (data:float[]) =
    Study.checkPositiveReal gamma
    let lookbackTotal = laguerreRsiLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let alpha = 1.0 - gamma
      let beta = -gamma

      let mutable prevL0 = 0.0
      let mutable prevL1 = 0.0
      let mutable prevL2 = 0.0
      let mutable prevL3 = 0.0

      let mutable L0 = 0.0
      let mutable L1 = 0.0
      let mutable L2 = 0.0
      let mutable L3 = 0.0

      for i in 0 .. endIdx do
        L0 <- alpha*data.[i] + gamma*L0
        L1 <- beta*L0 +  prevL0 + gamma*L1
        L2 <- beta*L1 +  prevL1 + gamma*L2
        L3 <- beta*L2 +  prevL2 + gamma*L3

        prevL0 <- L0
        prevL1 <- L1
        prevL2 <- L2
        prevL3 <- L3

        if i >= startIdx then
          let CU, CD =
            (0.0, 0.0)
              |> laguerreRsiAux L0 L1
              |> laguerreRsiAux L1 L2
              |> laguerreRsiAux L2 L3
          let denom = CU + CD
          out.[i-startIdx] <- if denom <> 0.0 then CU / denom else 0.0

      out
    )

  //===============================
  //
  // Leading indicator
  //
  //===============================

  let leadLookback = 1

  [<TradingStudy;
    Group("Cycle");
    Title("Ehlers Leading Indicator");
    Description("Returns the Ehlers leading indicator - from Cybernetic Analysis for stocks and futures (2004)");
  >]
  let lead alpha1 alpha2 (data:float[]) =
    Study.checkPositiveReal alpha1
    Study.checkPositiveReal alpha2
    let lookbackTotal = leadLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let a = alpha1 - 2.0
      let b = 1.0 - alpha1
      let c = 1.0 - alpha2

      let mutable lead = 0.0
      let mutable netLead = 0.0

      for i in startIdx .. endIdx do
        lead <- 2.0*data.[i] + a*data.[i-1] + b*lead
        netLead <- alpha2*lead + c*netLead
        out.[i-startIdx] <- netLead

      out
    )

  //===============================
  //
  // Fractal Moving Average
  //
  //===============================

  let framaLookback period = Stat.maxLookback period

  [<TradingStudy;
    Group("Smoothing");
    Title("Ehlers Fractal Moving Average");
    Description("Returns the Ehlers fractal moving average");
    Overlay;
  >]
  let frama period (data:float[]) =
    Study.checkPositiveIntMin1 period
    if period % 2 <> 0 then
      raise <| BadParam "period must be even"
    let lookbackTotal = framaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen
      let n = float period
      let n3 =
        let hh = Stat.max period data
        let ll = Stat.min period data
        Array.map2 (fun h l -> (h - l) / n) hh ll

      let halfPeriod = period / 2
      let n = float halfPeriod
      let n1, n2 =
        let hh = Stat.max halfPeriod data
        let ll = Stat.min halfPeriod data
        let n12 = Array.map2 (fun h l -> (h - l) / n) hh ll
        n12.[0..n12.Length-1 - halfPeriod], n12.[halfPeriod..]

      let mutable prevOut = data.[startIdx]
      let mutable dimen = 0.0
      let hpOffset = n1.Length - n3.Length
      let log2 = log 2.0
      for i in 0 .. Array.length n3 - 1 do
        let hpIdx = i + hpOffset
        if n1.[hpIdx] > 0.0 && n2.[hpIdx] > 0.0 && n3.[i] > 0.0 then
          dimen <- (log(n1.[hpIdx] + n2.[hpIdx]) - log(n3.[i])) / log2
        let alpha =
          exp (-4.60 * (dimen - 1.0)) |> max 0.01 |> min 1.0

        out.[i] <- alpha*data.[i+lookbackTotal] + (1.0-alpha) * prevOut
        prevOut <- out.[i]

      out
    )

Comments are closed.