Posts Tagged ‘finance’

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.

Reflection : retrieve information on the studies

In this part, we show how to use reflection to fetch the metadata attributes we have described in the first part of the technical analysis series.

namespace Trading.Chart

open System
open System.Reflection
open Microsoft.FSharp.Reflection

open Trading.Studies

//
//  Store info
//

module Reflection =

  type ParamInfo(p:ParameterInfo) =

    let name =
      let arr = p.GetCustomAttributes(typeof<DisplayNameAttribute>, false)
      if arr.Length = 0 then p.Name else (arr.[0] :?> DisplayNameAttribute).Name

    let defaultValue =
      let arr = p.GetCustomAttributes(typeof<DefaultValueAttribute>, false)
      if arr.Length = 0 then None else Some (arr.[0] :?> DefaultValueAttribute)

    let isBool =
      let arr = p.GetCustomAttributes(typeof<BoolAttribute>, false)
      arr.Length <> 0

    let numAttr =
      let arr = p.GetCustomAttributes(typeof<NumericAttribute>, false)
      if arr.Length = 0 then None else Some (arr.[0] :?> NumericAttribute)

    let allPositiveAttr =
      let arr = p.GetCustomAttributes(typeof<AllPositiveAttribute>, false)
      arr.Length <> 0

    member x.Name = name
    member x.Type = p.ParameterType.Name
    member x.Position = p.Position
    member x.IsBool = isBool
    member x.NumericAttribute = numAttr
    member x.AllPositiveAttribute = allPositiveAttr
    override x.ToString() =
      sprintf "{Name=%s; Type=%s; Position:%d; IsBool:%A; IsNumeric:%s}"
        x.Name x.Type x.Position x.IsBool
        ( match x.NumericAttribute with
          | None ->
              match x.Type.ToLower() with
              | "int32"
              | "double" -> "yes;"
              | _ -> "no;"
          | Some attr ->
              let min = if attr.MinValue = "nan" then "" else sprintf "min:%s; " attr.MinValue
              let max = if attr.MaxValue = "nan" then "" else sprintf "max:%s; " attr.MaxValue
              let step = if attr.Step = "nan" then "" else sprintf "step:%s;" attr.Step
              let np =
                if (min.Length > 0 || max.Length > 0 || step.Length > 0) then
                  sprintf " NumericParams:{%s%s%s}" min max step
                else ""
              sprintf "yes;%s" np
        )

  type StudyInfo = {
    Group : string
    Title : string
    Description : string
    DoesOverlay : bool
    HasMultipleInputSeries : bool
    Parameters : ParamInfo[]
    OutputTypes : string[]
    OutputSeriesNames : string[]
  } with
      static member create g t desc d multipleSeries p ot series = {
        Group = g
        Title = t
        Description = desc
        DoesOverlay = d
        HasMultipleInputSeries = multipleSeries
        Parameters = p
        OutputTypes = ot
        OutputSeriesNames = series
      }

//====================================================================
//
//  Extract the meta information from studies within the
//  assembly containing "Trading.Studies.TradingStudyAttribute"
//
//====================================================================

  let getMetainfo (m:MethodInfo) =
    let ps =
      m.GetParameters() |> Array.map (fun p -> new ParamInfo(p))

    let outTypes =
      let ty = m.ReturnType
      if FSharpType.IsTuple ty then
        FSharpType.GetTupleElements ty |> Array.map (fun ty -> ty.Name)
      else
        [|ty.Name|]

    let group =
      let arr = m.GetCustomAttributes((typeof<GroupAttribute>),false)
      if arr.Length = 0 then ""
      else (arr.[0] :?> GroupAttribute).Group

    let title =
      let arr = m.GetCustomAttributes((typeof<TitleAttribute>),false)
      if arr.Length = 0 then ""
      else (arr.[0] :?> TitleAttribute).Title

    let description =
      let arr = m.GetCustomAttributes((typeof<DescriptionAttribute>),false)
      if arr.Length = 0 then ""
      else (arr.[0] :?> DescriptionAttribute).Description

    let hasMultipleSeries =
      let arr = m.GetCustomAttributes((typeof<MultipleInputSeriesAttribute>),false)
      arr.Length <> 0

    let seriesNames =
      let arr = m.GetCustomAttributes((typeof<OutputSeriesNamesAttribute>),false)
      if arr.Length = 0 then [|"output"|]
      else (arr.[0] :?> OutputSeriesNamesAttribute).Series

    let doesOverlay =
      let arr = m.GetCustomAttributes((typeof<OverlayAttribute>),false)
      arr.Length <> 0

    StudyInfo.create group title description doesOverlay hasMultipleSeries ps outTypes seriesNames

  let isStudy (m:MemberInfo) =
    (m.GetCustomAttributes((typeof<TradingStudyAttribute>),false)).Length <> 0
    && (m :? MethodInfo)

  let getStudiesInfo() =
    typeof<Trading.Studies.TradingStudyAttribute>.Assembly.GetTypes()
      |> Array.filter FSharpType.IsModule
      |> Array.map (fun m ->
            m.GetMembers()
            |> Array.filter isStudy
          )
      |> Array.concat
      |> Array.map (fun m -> (m.Name, getMetainfo (m :?> MethodInfo)))
      |> Map.ofArray

Example use

do for KeyValue(name, info) in Trading.Chart.Reflection.getStudiesInfo()  do
      printfn "%s : %A" name info

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
    )

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.

Signal

namespace Trading.Studies

module Signal =

  let fisherTAux x =
    0.5 * log ((1.0 + x) / (1.0 - x))

  let fisherTLookback = 0

  let fisherT (data:float[]) =
    let lookbackTotal = fisherTLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      data
        |> Study.toRange (-0.99999) 0.99999
        |> Array.map fisherTAux
    )

  let fisherInv x =
    let x = exp (2.0 * x)
    (x - 1.0) / (x + 1.0)

  let fisherInvTLookback = 0

  let fisherInvT (data:float[]) =
    let lookbackTotal = fisherInvTLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      data
        |> Study.toRange (-0.99999) 0.99999
        |> Array.map fisherInv
    )

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.

Bands

namespace Trading.Studies

module Band =

  let bandsLookback = 0

  [<TradingStudy;
    Group("Bands");
    Title("Bands");
    Description("Returns an enveloppe built by scaling the input series");
    OutputSeriesNames("lower, upper");
    Overlay;
  >]
  let bands factor (data:float[]) =
    Study.checkPositiveReal factor
    let lookbackTotal = bandsLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      Array.map (fun x -> x * (1.0 - factor)) data,
      Array.map (fun x -> x * (1.0 + factor)) data
    )

  //Bollinger bands
  let bbandsLookback (ma:MA) period =
    max (Stat.stDevLookback period) (ma.lookback period)

  [<TradingStudy;
    Group("Bands");
    Title("Bollinger Bands");
    Description("Returns the Bollinger bands");
    OutputSeriesNames("lower, middle, upper");
    Overlay;
  >]
  let bbands (ma:MA) period stdevUp stdevDown (data:float[]) =
    Study.checkPositiveIntMin1 period
    Study.checkPositiveReal stdevUp
    Study.checkPositiveReal stdevDown
    let lookbackTotal = bbandsLookback ma period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute3 startIdx endIdx (fun outLen ->
      let mid = ma.create period data
      let stdev = Stat.stDev false period data
      let h = Array.map2 (fun mid stdev -> mid + stdevUp * stdev) mid stdev
      let l = Array.map2 (fun mid stdev -> mid - stdevDown * stdev) mid stdev
      l, mid, h
    )

  //Donchian channel
  let dchanLookback period =
    Stat.maxLookback period

  [<TradingStudy;
    Group("Bands");
    Title("Donchian Channel");
    Description("Returns the Donchian channel");
    OutputSeriesNames("lower, upper");
    Overlay;
  >]
  let dchan period (h:float[]) l =
    let lookbackTotal = dchanLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      (Stat.min period l, Stat.max period h)
    )

  //keltner bands
  let kbandsLookback (ma:MA) period =
    max (ma.lookback period + Price.typLookback) (Volatility.atrLookback period)

  [<TradingStudy;
    Group("Bands");
    Title("Keltner Bands");
    Description("Returns the Keltner bands");
    OutputSeriesNames("lower, middle, upper");
    Overlay;
  >]
  let kbands (ma:MA) period devUp devDown (h:float[]) l c =
    Study.checkPositiveIntMin1 period
    Study.checkPositiveReal devUp
    Study.checkPositiveReal devDown
    Study.checkHighLowClose h l c
    let lookbackTotal = kbandsLookback ma period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute3 startIdx endIdx (fun outLen ->
      let typ = Price.typ h l c
      let mid = ma.create period typ
      let atr = Volatility.atr period h l c
      let offset = atr.Length - mid.Length
      if offset > 0 then
        let h = Array.init mid.Length (fun i -> mid.[i] + atr.[i+offset] * devUp)
        let l = Array.init mid.Length (fun i -> mid.[i] - atr.[i+offset] * devDown)
        (l, mid, h)
      else
        //offset is negative, so we substract it
        let h = Array.init atr.Length (fun i -> mid.[i-offset] + atr.[i] * devUp)
        let l = Array.init atr.Length (fun i -> mid.[i-offset] - atr.[i] * devDown)
        (h, mid, l)
    )

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.

Volatility

namespace Trading.Studies

module Volatility =

  let tHighLookback = 1

  [<TradingStudy;
    Group("Volatility");
    Title("True High");
    Description("Returns the true high");
    Overlay;
  >]
  let tHigh h (c:float[]) =
    Study.checkHighLow h c
    let lookbackTotal = tHighLookback
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i -> max c.[i] h.[i + 1])
    )

  let tLowLookback = 1

  [<TradingStudy;
    Group("Volatility");
    Title("True Low");
    Description("Returns the true low");
    Overlay;
  >]
  let tLow l (c:float[]) =
    Study.checkHighLow c l
    let lookbackTotal = tLowLookback
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i -> min c.[i] l.[i + 1])
    )

  let tRangeLookback = 1

  [<TradingStudy;
    Group("Volatility");
    Title("True Range");
    Description("Returns the true range");
    Overlay;
  >]
  let tRange h l (c:float[]) =
    let lookbackTotal = tRangeLookback
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let th = tHigh h c
      let tl = tLow l c
      Array.map2 ( - ) th tl
    )

  let atrLookback period =
    MA.wilmaLookback period + tRangeLookback

  [<TradingStudy;
    Group("Volatility");
    Title("Average True Range");
    Description("Returns the average true range");
    Overlay;
  >]
  let atr period h l c =
    tRange h l c |> MA.wilma period 

  //John Forman - Technical Analysis of Stock & Commodities - May 2006
  let natrLookback period = atrLookback period

  [<TradingStudy;
    Group("Volatility");
    Title("Normalized Average True Range");
    Description("Returns the normalized average true range");
  >]
  let natr period h l c =
    atr period h l c |> Array.mapi (fun i x ->
      if c.[i + period] <> 0.0 then 100.0 * (x / c.[i + period]) else 0.0
    )     

  //Chaikin Volatility
  let chVolLookback maPeriod rocPeriod =
    MA.emaLookback maPeriod + Osc.rocLookback rocPeriod

  [<TradingStudy;
    Group("Volatility");
    Title("Chaikin Volatility");
    Description("Returns the Chaikin volatility");
  >]
  let chVol maPeriod rocPeriod (h:float[]) l =
    Study.checkHighLow h l
    Array.map2 (fun h l -> h - l) h l
      |> MA.ema maPeriod
      |> Osc.roc true nan rocPeriod

  //Olivier Seban Super Trend
  //See : http://hk-lisse.over-blog.com/article-19983903.html
  [<TradingStudy;
    Group("Volatility");
    Title("Seban Super Trend");
    Description("Returns Seban's Super Trend");
    Overlay;
  >]
  let superTrendLookback period =
    atrLookback period + 1

  let superTrend period devUp devDn high low (close:float[]) =
    Study.checkPositiveIntMin1 period
    Study.checkPositiveReal devUp
    Study.checkPositiveReal devDn
    Study.checkHighLowClose high low close
    let lookbackTotal = superTrendLookback period
    let startIdx = lookbackTotal
    let endIdx = close.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let atr = atr period high low close
      let atrLookback = atrLookback period
      let median =Price.med high low

      let mutable trend = 1
      let mutable prevTrend = 1
      let mutable prevUp = median.[startIdx-1] + devUp * atr.[0]
      let mutable prevDown = median.[startIdx-1] - devDn * atr.[0]

      for i in startIdx .. endIdx do
        let atrIdx = i - atrLookback
        let mutable up = median.[i] + devUp * atr.[atrIdx]
        let mutable down = median.[i] - devDn * atr.[atrIdx]

        if close.[i] > prevUp then trend <- 1
        elif close.[i] < prevDown then trend <- -1

        let flag = if trend < 0 && prevTrend > 0 then 1 else 0
        let flagh = if trend > 0 && prevTrend < 0 then 1 else 0

        if trend > 0 && down < prevDown then down <- prevDown
        if trend < 0 && up > prevUp then up <- prevUp

        if flag = 1 then up <- median.[i] + devUp * atr.[atrIdx]
        if flagh = 1 then down <- median.[i] - devDn * atr.[atrIdx]

        out.[i-startIdx] <- if trend = 1 then down else up
        prevTrend <- trend
        prevUp <- up
        prevDown <- down

      out
    )

  let dmAux highPrev lowPrev highToday lowToday=
    let deltaHighs = highToday - highPrev
    let deltaLows = lowPrev - lowToday
    if (deltaHighs = deltaLows) || (deltaHighs < 0.0 && deltaLows < 0.0) then  (0.0, 0.0)
    elif deltaHighs > deltaLows then (0.0, deltaHighs)
    else (deltaLows, 0.0)

  let dmLookback period = MA.wilmaLookback period

  [<TradingStudy;
    Group("Volatility");
    Title("Directional Movement DM");
    Description("Returns the directional movement DM");
    OutputSeriesNames("DM-, DM+");
  >]
  let dm period high (low:float[]) =
    Study.checkPositiveIntMin1 period
    Study.checkHighLow high low
    let lookbackTotal = dmLookback period
    let startIdx = lookbackTotal
    let endIdx = low.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let lookback = MA.wilmaLookback period
      //in Wilder's approach to compute the ADX, rather than
      //using the first n values : the first value is assumed
      //to be 0.0, and therefore, only the actual n-1 first values
      //are used in the wilder summations.
      //
      //For instance, the first value of a 5-period wilder summation
      //will be computed as the sum of the first four values of the
      //input series. Other values are computed normally.
      //
      //We therefore create a 0.0-filled array where the first item
      //is never computed, and thus left at 0.0, prior
      let n = outLen + lookback + (* 0.0 adjustment *) 1
      let dmPlus = Array.zeroCreate n
      let dmMinus = Array.zeroCreate n
      let startIdx = startIdx - lookback
      //leave the first value, 0.0, untouched
      for today in startIdx + 1 .. endIdx do
        let prev = today - 1
        let (deltaLows, deltaHighs) = dmAux high.[prev] low.[prev] high.[today] low.[today]
        let outIdx = today - startIdx
        dmMinus.[outIdx] <- deltaLows
        dmPlus.[outIdx] <- deltaHighs

      (Misc.wilSum period dmMinus, Misc.wilSum period dmPlus)
    )

  let diLookback period = dmLookback period + 1

  [<TradingStudy;
    Group("Volatility");
    Title("Directional Movement DI");
    Description("Returns the directional index DI");
    OutputSeriesNames("DI-, DI+");
  >]
  let di period high low (close:float[])  =
    Study.checkPositiveIntMin1 period
    Study.checkHighLowClose high low close
    let lookbackTotal = diLookback period
    let startIdx = lookbackTotal
    let endIdx = close.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let diPlus = Array.zeroCreate outLen
      let diMinus = Array.zeroCreate outLen

      let dmMinus, dmPlus = dm period high low

      let summedTRange =
        let tr = tRange high low close
        //Create an array with the same number of items + 1
        let tmp = Array.zeroCreate (tr.Length + 1)
        //copy the true range into this array, starting at i=1,
        //thus leaving the first value at 0.0
        Array.blit tr 0 tmp 1 tr.Length
        //Compute the wilder summation over this expanded array
        Misc.wilSum period tmp

      //Ignore the first DI value to compensate for the 0.0 values
      //that were added for each wilder summation, both in the dm
      //and in the true range.
      //Compute other values normally.
      for i in 1 .. summedTRange.Length - 1 do
        let outIdx = i - 1
        diPlus.[outIdx] <- 100.0 * dmPlus.[i] / summedTRange.[i]
        diMinus.[outIdx] <- 100.0 * dmMinus.[i] / summedTRange.[i]

      (diMinus, diPlus)
    )

  let dxLookback period = diLookback period

  [<TradingStudy;
    Group("Volatility");
    Title("Directional Movement DX");
    Description("Returns the directional movement DX");
  >]
  let dx period h l c =
    let diMinus, diPlus = di period h l c
    Array.map2 (fun m p ->
      let diSum = p + m
      if diSum = 0.0 then 0.0 else abs (p - m) / diSum * 100.0
    ) diPlus diMinus

  let adxLookback dxPeriod adxPeriod =
    dxLookback dxPeriod + MA.wilmaLookback adxPeriod

  [<TradingStudy;
    Group("Volatility");
    Title("Directional Movement ADX");
    Description("Returns the average directional movement ADX");
  >]
  let adx dxPeriod adxPeriod h l (c:float[]) =
    let lookbackTotal = adxLookback dxPeriod adxPeriod
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      dx dxPeriod h l c |> MA.wilma adxPeriod
    )

  let adxrLookback dxPeriod adxPeriod adxrPeriod =
    adxLookback dxPeriod adxPeriod + MA.wilmaLookback adxrPeriod

  [<TradingStudy;
    Group("Volatility");
    Title("Directional Movement ADXR");
    Description("Returns the average directional movement rating ADXR");
  >]
  let adxr dxPeriod adxPeriod adxrPeriod h l (c:float[]) =
    let lookbackTotal = adxrLookback dxPeriod adxPeriod adxrPeriod
    let startIdx = lookbackTotal
    let endIdx = c.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      adx dxPeriod adxPeriod h l c |> MA.wilma adxrPeriod
    )

  //Parabolic stop and reverse

  let sarLookback = 1

  let isLong startsAsLong (high:float[]) (low:float[]) =
    if startsAsLong <> 0 then
      startsAsLong > 0
    else
        let dmMinus, dmPlus = dmAux high.[0] low.[0] high.[1] low.[1]
        dmPlus >= dmMinus

  //Make sure the value is not too high or too low
  let minLow (low:float[]) idx v =
    if idx > 1 then
      min v low.[idx] |> min low.[idx - 1]
    elif idx = 1 then
      min v low.[idx]
    else
      invalidArg "idx" "idx must be equal or larger than 1"

  let maxHigh (high:float[]) idx v =
    if idx > 1 then
      max v high.[idx] |> max high.[idx - 1]
    elif idx = 1 then
      max v high.[idx]
    else
      invalidArg "idx" "idx must be equal or larger than 1"

  [<TradingStudy;
    Group("Volatility");
    Title("Parabolic SAR - Advanced");
    Description("Returns the parabolic stop and reverse with advanced parameters");
    Overlay;
  >]
  let sarExtended startsAsLong offsetOnReverse aFLongInit aFLongStep aFLongMax aFShortInit aFShortStep aFShortMax (high:float[]) low =
    Study.checkPositiveReal offsetOnReverse
    Study.checkPositiveReal aFLongInit
    Study.checkPositiveReal aFLongStep
    Study.checkPositiveReal aFLongMax
    Study.checkPositiveReal aFShortInit
    Study.checkPositiveReal aFShortStep
    Study.checkPositiveReal aFShortMax
    Study.checkHighLow high low
    let lookbackTotal = sarLookback
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      //
      // Sanitize arguments
      //
      let aFLongInit = min aFLongInit aFLongMax
      let aFShortInit = min aFShortInit aFShortMax

      let aFLongStep = min aFLongStep (aFLongMax - aFLongInit)
      let aFShortStep = min aFShortStep (aFShortMax - aFShortInit)

      let aFLongMax = max aFLongInit aFLongMax
      let aFShortMax = max aFShortInit aFShortMax

      let mutable aFLong = aFLongInit
      let mutable aFShort = aFShortInit

      let offsetOnReverseToShortCoeff = 1.0 + offsetOnReverse
      let offsetOnReverseToLongCoeff = 1.0 - offsetOnReverse

      //
      // Initialize the algorithm state values
      //
      let mutable isLong = isLong startsAsLong high low
      let mutable ep = 0.0
      let mutable sar = 0.0

      let prev = startIdx - 1
      if isLong then
        ep <- high.[startIdx]
        sar <- low.[prev]
      else
        ep <- low.[startIdx]
        sar <- high.[prev]

      for today in startIdx .. endIdx do
        let outIdx = today - startIdx
        if isLong then
          if low.[today] < sar then
            //init short
            isLong <- false
            sar <- (maxHigh high today ep) * offsetOnReverseToShortCoeff
            //store the sar
            out.[outIdx] <- sar
            //compute next sar
            ep <- low.[today]
            aFShort <- aFShortInit
            sar <- sar + aFShort * (ep - sar)
          else
            //store the sar
            out.[outIdx] <- sar
            //update long
            if high.[today] > ep then
              ep <- high.[today]
              aFLong <- min aFLongMax (aFLong + aFLongStep)
            //compute next long
            let newSar = sar + aFLong * (ep - sar)
            sar <- minLow low today newSar
        else
          if high.[today] > sar then
            //init long
            isLong <- true
            sar <- (minLow low today ep) * offsetOnReverseToLongCoeff
            //store the sar
            out.[outIdx] <- sar
            //compute next sar
            ep <- high.[today]
            aFLong <- aFLongInit
            sar <- sar + aFLong * (ep - sar)
          else
            //store the sar
            out.[outIdx] <- sar
            //update short
            if low.[today] < ep then
              ep <- low.[today]
              aFShort <- min aFShortMax (aFShort + aFShortStep)
            //compute next short
            let newSar = sar + aFShort * (ep - sar)
            sar <- maxHigh high today newSar 

      out
    )

  [<TradingStudy;
    Group("Volatility");
    Title("Parabolic SAR");
    Description("Returns the parabolic stop and reverse");
    Overlay;
  >]
  let sar aFInit aFStep aFMax high low =
    sarExtended 0 0.0 aFInit aFStep aFMax aFInit aFStep aFMax high low

  let maxPeriod p1 p2 p3 =
    max p1 p2 |> max p3

  let largestLookback p1 p2 p3 =
    maxPeriod p1 p2 p3 |> MA.smaLookback

  let ultOscLookback p1 p2 p3 =
    largestLookback p1 p2 p3 + tRangeLookback

  [<TradingStudy;
    Group("Volatility");
    Title("Ultimate Oscillator");
    Description("Returns the ultimate oscillator");
  >]
  let ultOsc p1 p2 p3 high low (close:float[]) =
    Study.checkPositiveIntMin1 p1
    Study.checkPositiveIntMin1 p2
    Study.checkPositiveIntMin1 p3
    Study.checkHighLowClose high low close
    let lookbackTotal = ultOscLookback p1 p2 p3
    let startIdx = lookbackTotal
    let endIdx = close.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let periods = [|p1; p2; p3|] |> Array.sort
      let p1 = periods.[0] //shortest period
      let p2 = periods.[1]
      let p3 = periods.[2] //longest period

      //make true range and delta start so that the slowest
      //moving average will yield a result at startIdx
      let trueRange = tRange high low close
      let delta =
        let tlow = tLow low close
        let offset = close.Length - tlow.Length
        Array.init tlow.Length (fun i -> close.[i+offset] - tlow.[i])

      let a1 = Math.sum p1 delta
      let a2 = Math.sum p2 delta
      let a3 = Math.sum p3 delta

      let b1 = Math.sum p1 trueRange
      let b2 = Math.sum p2 trueRange
      let b3 = Math.sum p3 trueRange

      let offset13 = a1.Length - a3.Length
      let offset23 = a2.Length - a3.Length

      let alpha = 100.0 / 7.0

      Array.init outLen (fun i ->
        let a = a1.[i+offset13] / b1.[i+offset13]
        let b = a2.[i+offset23] / b2.[i+offset23]
        let c = a3.[i] / b3.[i]
        (4.0 * a + 2.0 * b + c) * alpha
      )
    )

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.

Misc

namespace Trading.Studies

module Misc =

  let slopeLookback (period:int) = period

  (* (value_n - value_0) / numberOfPeriods *)
  [<TradingStudy;
    Group("Misc");
    Title("Slope");
    Description("Returns the unit slope between n observations");
  >]
  let slope period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = slopeLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let n = float period
      Array.init (data.Length - period) (fun i -> (data.[i + period] - data.[i]) / n)
    )

  let wilSumLookback period = period - 1

  [<TradingStudy;
    Group("Misc");
    Title("Wilder summation");
    Description("Returns the Wilder summation");
  >]
  let wilSum period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = wilSumLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let coeff = 1.0 - (1.0 / float period)

      for i in startIdx - lookbackTotal .. startIdx do
        out.[0] <- out.[0] + data.[i]

      for i in startIdx + 1 .. endIdx do
        let outIdx = i - startIdx
        out.[outIdx] <- coeff * out.[outIdx-1] + data.[i]
      out
    )

  //==========================================
  //
  // NOT CHECKED
  //
  //==========================================  

  let cftppLookback = 1

  ///Chicago Floor Trading Pivotal Point

  [<TradingStudy;
    Group("Misc");
    Title("Chicago Floor Trading Pivotal Point");
    Description("Returns the Chicago floor trading pivotal point");
    OutputSeriesNames("s2, s1, r1, r2");
  >]
  let cftpp (h:float[]) l c =
    Study.checkHighLowClose h l c
    let lookbackTotal = cftppLookback
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute4 startIdx endIdx (fun outLen ->
      let typ = Price.typ h l c
      ( Array.init outLen (fun i -> typ.[i+1] - h.[i] + l.[i]),
        Array.init outLen (fun i -> 2.0 * typ.[i+1] - h.[i]),
        Array.init outLen (fun i -> 2.0 * typ.[i+1] - l.[i]),
        Array.init outLen (fun i -> typ.[i+1] + h.[i] - l.[i])
      )
    )

  let pvrLookback (period:int) = period

  [<TradingStudy;
    Group("Volume");
    Title("Price Volume Rank");
    Description("Returns the price volume rank");
  >]
  let pvr period (data:float[]) volume =
    Study.checkPositiveIntMin1 period
    Study.checkSameInputLength [volume; data]
    Study.checkVolume volume
    let lookbackTotal = pvrLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //1 : price and volume increase
      //2 : price increase without volume
      //3 : price decrease without volume
      //4 : price decrease with volume
      Array.init outLen (fun prev ->
        let today = prev + period
        if data.[today] > data.[prev] then
          if volume.[today] > volume.[prev] then 1.0 else 2.0
        elif
          volume.[today] > volume.[prev] then 4.0
        else
          3.0
      )
    )

  ///Ease of movement
  let eomLookback = 1

  [<TradingStudy;
    Group("Volume");
    Title("Ease of movement");
    Description("Returns the ease of movement indicator");
  >]
  let eom volumeDivisor h l volume =
    Study.checkPositiveReal volumeDivisor
    Study.checkSameInputLength [volume; h]
    Study.checkHighLow h l
    Study.checkVolume volume
    let lookbackTotal = eomLookback
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun prev ->
        let today = prev + 1
        let delta = h.[today] - l.[today]
        if delta <> 0.0 && volume.[today] <> 0.0 then
          (0.5 * (h.[today] - l.[today] - h.[prev] + l.[prev])) / ((volume.[prev] / volumeDivisor) / delta)
        else
          0.0
      )
    )

  ///Trend score
  let tsLookback period =
    Math.sumLookback period + 1

  [<TradingStudy;
    Group("Misc");
    Title("Trend score");
    Description("Returns the ease of movement indicator");
  >]
  let ts period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = tsLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun prev ->
        let today = prev + 1
        if data.[today] >= data.[prev] then 1.0 else (-1.0)
      ) |> Math.sum period
    )

  (*  Tushar S. Chande - March, 1992 - Technical Analysis of Stocks & Commodities magazine
      A standard deviation was used as the Volatility Index. 

      In his October, 1995 article in the same magazine, Chande modified the VIDYA to use
      his own Chande momentum Osc (CMO) as the Volatility Index. 

      Examples of volatility indexes :
      - standard deviation (e.g., 9 period)
      - standard deviation percentage oscillator (e.g., 10 period divd by 50 period)
      - Chande momentum Osc (e.g., 9 period)
  *)

  let vidyaLookback period =
    Osc.cmoLookback period - 1

  [<TradingStudy;
    Group("Smoothing");
    Title("Vidya MA");
    Description("Returns the Vidya (or Variable) moving average");
  >]
  let vidya period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = tsLookback 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 coeff = 2.0 / (n + 1.0)

      let cmo = Osc.cmo period data

      for i in startIdx - lookbackTotal .. startIdx do
        out.[0] <- out.[0] + (data.[i] / n)

      for i in startIdx + 1 .. endIdx do
        let outIdx = i - startIdx
        let cmoIdx = outIdx
        let alpha = coeff * cmo.[cmoIdx]
        out.[outIdx] <- alpha * data.[i] + (1.0 - alpha) * out.[outIdx-1]

      out
    )

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.

Oscillators

namespace Trading.Studies

module Osc =

  let wmomLookback period = BaseStudies.wmomLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Weighted Momentum");
    Description("Returns the weighted momentum");
  >]
  let wmom coeffToday coeffLag period data =
    BaseStudies.wmom coeffToday coeffLag period data

  let momLookback period = BaseStudies.momLookback period
  [<TradingStudy;
    Group("Cycle");
    Title("Momentum");
    Description("Returns the momentum");
  >]
  let mom period data = BaseStudies.mom period data

  let rocLookback period = BaseStudies.rocLookback period
  [<TradingStudy;
    Group("Cycle");
    Title("Rate of Change");
    Description("Returns the rate of change");
  >]
  let roc isBase100 divByZeroDefaultValue period data =
    BaseStudies.roc isBase100 divByZeroDefaultValue period data

  (*  The Relative momentum Index is a variation on the Relative Strength Index.
      To determine up and down days, the RSI uses the close compared to the previous close.
      The RMI uses the close compared to the close n days ago.
      An RMI with a time period of 1 is equal to the RSI. 

      The Relative momentum Index was developed by Roger Altman and was introduced
      in his article in the February, 1993 issue of Technical Analysis of Stocks & Commodities magazine.

      Note that since RMI is more "versatile" than RSI, we define the RSI as a function of the RMI.

      See the RSI module for more information.

      RMI = 100 - (100 / (1 + RM)) where RM = average gain / average loss over the period
      <=> RMI = 100 * (avg_gain / (avg_gain+avg_loss))
      average gain = moving average of the up moves
      average loss = moving average of the down moves
      where the moving averages have a period = average_period
      and up_move_i and down_move_i are based upon value_i - value_(i-momentum_period)

      Note that since gains (resp. losses) are smoothed, there is a convergence effect :
      the first values of the RMI are less smoothed than the following ones.

      Therefore, the further one advances in the calculation, the less impact this initial
      divergence has, the more stability you have.
  *)

  let gainsLookback period = period - 1

  [<TradingStudy;
    Group("Misc");
    Title("Gains");
    Description("Returns the n-period gains");
  >]
  let gains period (data:float[]) =
    let lookbackTotal = gainsLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      data
        |> mom period
        |> Array.map (max 0.0)
    )

  let lossesLookback period = period - 1
  [<TradingStudy;
    Group("Misc");
    Title("Losses");
    Description("Returns the n-period losses");
  >]
  let losses period (data:float[]) =
    let lookbackTotal = lossesLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      data
        |> mom period
        |> Array.map (min 0.0)
    )

  let rmiLookback momPeriods avgPeriods =
    momLookback momPeriods + MA.wilmaLookback avgPeriods

  let rmiAux gain loss =
    if gain <> loss then 100.0 * (gain / (gain - loss)) else 0.0  

  [<TradingStudy;
    Group("Cycle");
    Title("Relative Momentum Index");
    Description("Returns the relative momentum index");
  >]
  let rmi momPeriods avgPeriods (data:float[]) =
    let lookbackTotal = rmiLookback momPeriods avgPeriods
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let gains =
        data
          |> gains momPeriods
          |> MA.wilma avgPeriods
      let losses =
        data
          |> losses momPeriods
          |> MA.wilma avgPeriods
      Array.map2 (fun gain loss -> rmiAux gain loss) gains losses
    )

  let rsiLookback period = rmiLookback 1 period

  [<TradingStudy;
    Group("Cycle");
    Title("Relative Strength Index");
    Description("Returns the relative strength index");
  >]
  let rsi period data =
    rmi 1 period data

  //Tuschar Chande momentum oscillator
  let cmoLookback avgPeriods =
    momLookback 1 + MA.wilmaLookback avgPeriods

  let cmoAux gain loss =
    if gain <> loss then
      100.0 * (gain + loss) / (gain - loss)
    else
      0.0

  [<TradingStudy;
    Group("Cycle");
    Title("Chande Momentum Oscillator");
    Description("Returns the Chande momentum oscillator");
  >]
  let cmo avgPeriods (data:float[]) =
    let lookbackTotal = cmoLookback avgPeriods
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let gains =
        data
          |> gains 1
          |> MA.wilma avgPeriods
      let losses =
        data
          |> losses 1
          |> MA.wilma avgPeriods
      Array.map2 (fun gain loss -> cmoAux gain loss) gains losses
    )

  // Donald R. Lambert - CCI
  let cciLookback period = MA.smaLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Commodity Channel Index");
    Description("Returns the cmmodity channel index");
  >]
  let cci period (h:float[]) l c =
    let lookbackTotal = cciLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let typ = Price.typ h l c
      let ma = MA.sma period typ
      let absdev = Stat.avgDev period typ
      let offset = typ.Length - ma.Length
      Array.init ma.Length (fun i ->
        if absdev.[i] <> 0.0 then (typ.[i + offset] - ma.[i]) / (0.015 * absdev.[i]) else 0.0
      )
    )

  let trixLookback period =
    3 * MA.emaLookback period + rocLookback 1

  [<TradingStudy;
    Group("Cycle");
    Title("Trix");
    Description("Returns the Trix");
  >]
  let trix period (data:float[]) =
    let lookbackTotal = trixLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      data
        |> MA.ema period
        |> MA.ema period
        |> MA.ema period
        |> roc true 100.0 1
    )

  ///Williams %R
  let willRLookback period = period - 1

  let willRAux h l c =
    let delta = h - l
    if delta <> 0.0 then (-100.0) * (h - c) / delta else 0.0

  [<TradingStudy;
    Group("Cycle");
    Title("Williams %R");
    Description("Returns the Williams %R");
  >]
  let willR period h l c =
    Study.checkPositiveIntMin1 period
    Study.checkHighLowClose h l c
    let lookbackTotal = willRLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let hh = Stat.max period h
      let ll = Stat.max period l
      let offset = h.Length - hh.Length
      Array.init outLen (fun i -> willRAux h.[i] l.[i] c.[i+offset])
    )

  ///Stochastics Fast K
  let stochFastKLookback period = Stat.maxLookback period

  let stochFastKAux hh ll c =
    let delta = hh - ll
    if delta <> 0.0 then 100.0 * (c - ll) / delta else 0.0

  [<TradingStudy;
    Group("Cycle");
    Title("Stochastics Fast K");
    Description("Returns the stochastics fast K");
  >]
  let stochFastK period h l c =
    Study.checkPositiveIntMin1 period
    Study.checkHighLowClose h l c
    let lookbackTotal = stochFastKLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let hh = Stat.max period h
      let ll = Stat.min period l
      let offset = h.Length - hh.Length
      Array.init outLen (fun i -> stochFastKAux hh.[i] ll.[i] c.[i+offset])
    )

  ///Stochastics Fast (Fast K + Slow K)
  let stochFastLookback fastKPeriod (ma:MA) slowKPeriod =
    stochFastKLookback fastKPeriod + ma.lookback slowKPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Stochastics Fast K/Slow K");
    Description("Returns the stochastics fast K and slow K");
    OutputSeriesNames("fast K, slow K");
  >]
  let stochFast fastKPeriod (ma:MA) slowKPeriod (h:float[]) l c =
    let lookbackTotal = stochFastLookback fastKPeriod ma slowKPeriod
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let fastK = stochFastK fastKPeriod h l c
      let slowK = ma.create slowKPeriod fastK
      let offset = fastK.Length - slowK.Length
      fastK.[offset..], slowK
    )

  ///Stochastics Slow (Fast D + Slow D, where Fast D = Slow K)
  let stochSlowLookback fastKPeriod maK slowKPeriod (maD:MA) slowDPeriod =
    stochFastLookback fastKPeriod maK slowKPeriod + maD.lookback slowDPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Stochastics Slow K/Slow D");
    Description("Returns the stochastics slow K and slow D");
    OutputSeriesNames("slow K, slow D");
  >]
  let stochSlow fastKPeriod maK slowKPeriod maD slowDPeriod (h:float[]) l c =
    let lookbackTotal = stochSlowLookback fastKPeriod maK slowKPeriod maD slowDPeriod
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let fastK, slowK = stochFast fastKPeriod maK slowKPeriod h l c
      let slowD = maD.create slowDPeriod slowK
      let offset = slowK.Length - slowD.Length
      slowK.[offset..], slowD
    )

  ///Moving Average Convergence Divergence
  let macdLookback (ma:MA) slowPeriod signalPeriod =
    ma.lookback slowPeriod + ma.lookback signalPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("MACD");
    Description("Returns the moving average convergence-divergence");
    OutputSeriesNames("macd, signal, histogram");
  >]
  let macd (ma:MA) fastPeriod slowPeriod signalPeriod (data:float[]) =
    if fastPeriod > slowPeriod then
      invalidArg "fastPeriod" "fastPeriod > slowPeriod"
    let lookbackTotal = macdLookback ma slowPeriod signalPeriod
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute3 startIdx endIdx (fun outLen ->
      let fastMA = ma.create fastPeriod data
      let slowMA = ma.create slowPeriod data
      let offset = fastMA.Length - slowMA.Length
      let macd = Array.init slowMA.Length (fun i -> fastMA.[i + offset] - slowMA.[i])
      let signal = ma.create signalPeriod macd
      let offset = macd.Length - signal.Length
      let hist = Array.init signal.Length (fun i -> macd.[i + offset] - signal.[i])
      macd.[offset..], signal, hist
    )

  ///Detrended price oscillator
  let dpoPeriod period = period / 2 + 1

  let dpoLookback period =
    MA.smaLookback (dpoPeriod period)

  [<TradingStudy;
    Group("Cycle");
    Title("Detrended Price Oscillator");
    Description("Returns the detrended price oscillator");
  >]
  let dpo period data =
    let ma = MA.sma (dpoPeriod period) data
    let offset = data.Length - ma.Length
    Array.init ma.Length (fun i -> data.[i + offset] - ma.[i])

  ///Chande Dynamic Momentum Index
  ///Tushar S. Chande and Stanley Kroll and is described in their 1994 book The New Technical Trader
  let cdmiLookback =
    Stat.stDevLookback 5 + MA.smaLookback 10

  [<TradingStudy;
    Group("Cycle");
    Title("Dynamic Momentum Index");
    Description("Returns the Chande dynamic momentum index");
  >]
  let cdmi data =
    let sd = Stat.stDev true 5 data
    let asd = MA.sma 10 sd
    let offset = sd.Length - asd.Length
    Array.init asd.Length (fun i -> 14.0 * asd.[i] / sd.[i + offset])

  ///On-Balance Volume
  let obvLookback = 0

  [<TradingStudy;
    Group("Volume");
    Title("On-Balance Volume");
    Description("Returns the on-balance volume");
  >]
  let obv (data:float[]) volume =
    Study.checkSameInputLength [volume; data]
    Study.checkVolume volume
    let lookbackTotal = obvLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      out.[0] <- volume.[startIdx]

      for today in startIdx + 1 .. endIdx do
        let outIdx = today - startIdx
        if data.[today] > data.[today - 1] then
          out.[outIdx] <- out.[outIdx-1] + volume.[today]
        elif data.[today] < data.[today - 1] then
          out.[outIdx] <- out.[outIdx-1] - volume.[today]
        else
          out.[outIdx] <- out.[outIdx-1]

      out
    )

  ///Accumulation / Distribution
  let adLookback = 0

  //CLV = ((C - L) - (H - C)) / (H - L)
  //    = (2C - H - L) / (H-L)
  let volumeClv h l c v =
    let delta = h - l
    if delta <> 0.0 then (2.0 * c - h - l) / delta * v else 0.0

  let adLineClv h l c volume =
    Study.checkSameInputLength [volume; h]
    Study.checkVolume volume
    Study.checkHighLowClose h l c
    let lookbackTotal = adLookback
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i -> volumeClv h.[i] l.[i] c.[i] volume.[i])
    )  

  [<TradingStudy;
    Group("Volume");
    Title("Accumulation / Distribution");
    Description("Returns the Chande accumulation / distribution");
  >]
  let ad h l c volume =
    let adLine = adLineClv h l c volume
    let startIdx = adLookback
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen
      out.[0] <- adLine.[0]
      for today in startIdx + 1.. endIdx do
        let outIdx = today - startIdx
        out.[outIdx] <- out.[outIdx - 1] + adLine.[today]

      out
    )

  ///Chaikin Osc
  let chOscLookback slowPeriod =
    adLookback + MA.emaLookback slowPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Chaikin Oscillator");
    Description("Returns the Chaikin oscillator");
  >]
  let chOsc fastPeriod slowPeriod (h:float[]) l c volume =
    if fastPeriod > slowPeriod then
      raise <| BadParam "fast period > slowPeriod"
    let lookbackTotal = chOscLookback slowPeriod
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let ad = ad h l c volume
      let fast = MA.ema fastPeriod ad
      let slow = MA.ema slowPeriod ad
      let offset = fast.Length - slow.Length
      Array.init slow.Length (fun i -> fast.[i + offset] - slow.[i])
    )  

  ///Chaikin Money Flow
  let chMfLookback period =
    adLookback + Math.sumLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Chaikin Money Flow");
    Description("Returns the Chaikin money flow");
  >]
  let chMf period (h:float[]) l c volume =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = chMfLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      //adLineClv has 0 lookback, so we can use a map2 afterward
      let ad = adLineClv h l c volume |> Math.sum period
      let vol = Math.sum period volume
      Array.map2 ( / ) ad vol
    )

  ///Balance of Power
  let bpowLookback = 0

  [<TradingStudy;
    Group("Cycle");
    Title("Balance of Power");
    Description("Returns the balance of power");
  >]
  let bpow o h l c =
    Study.checkOpenHighLowClose o h l c
    let lookbackTotal = bpowLookback
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let idx = startIdx + i
        let delta = h.[idx] - l.[idx]
        if delta <> 0.0 then (c.[idx] - o.[idx]) / delta else 0.0
      )
    )  

  let aroonLookback period = Stat.maxLookback period + 1

  [<TradingStudy;
    Group("Cycle");
    Title("Aroon");
    Description("Returns the Aroon indicator");
    OutputSeriesNames("aroon down, aroon up");
  >]
  let aroon period h l =
    Study.checkPositiveIntMin1 period
    Study.checkHighLow h l
    let lookbackTotal = aroonLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute2 startIdx endIdx (fun outLen ->
      let extremasPeriod = period + 1
      let highestHighs = Stat.maxIdx extremasPeriod h
      let lowestLows = Stat.minIdx extremasPeriod l
      let aroonUp = Array.zeroCreate outLen
      let aroonDown = Array.zeroCreate outLen
      let n = float period
      let coeff = 100.0 / n
      let f pos extremaIdx = coeff * (n - float (pos - extremaIdx))

      Array.iteri2 (fun i hh ll ->
        let g = f (startIdx + i)
        aroonUp.[i] <- g hh
        aroonDown.[i] <- g ll
      ) highestHighs lowestLows

      aroonDown, aroonUp
    )  

  let aroonOscLookback period = aroonLookback period

  [<TradingStudy;
    Group("Cycle");
    Title("Aroon oscillator");
    Description("Returns the Aroon oscillator");
  >]
  let aroonOsc period h l =
    let down, up = aroon period h l
    Array.map2 (-) up down

  ///Money Flow Index
  let mfiLookback period =
    Price.typLookback + Math.sumLookback period

  let mfiAux plus minus =
    //100 - (100 / (1 + MoneyRatio)) <=> 100 * sum+ / (sum+ + sum-)
    //to avoid excessively large values, we ilter based on the denominator
    let mr = plus / minus
    if mr = 1.0 then 0.0 else 100.0 * (mr / (1.0 + mr))

  [<TradingStudy;
    Group("Cycle");
    Title("Money Flow Index");
    Description("Returns the money flow index");
  >]
  let mfi period h l c volume =
    Study.checkPositiveIntMin1 period
    Study.checkVolume volume
    Study.checkHighLowClose h l c
    let lookbackTotal = aroonLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let tp = Price.typ h l c
      let mf = Array.map2 ( * ) tp volume
      let mfPlus = Array.zeroCreate tp.Length
      let mfMinus = Array.zeroCreate tp.Length
      for i in 1 .. tp.Length - 1 do
        if tp.[i] > tp.[i - 1] then
          mfPlus.[i] <- mfPlus.[i-1] + mf.[i]
        else
          mfMinus.[i] <- mfMinus.[i-1] + mf.[i]
      Array.map2 mfiAux (Math.sum period mfPlus) (Math.sum period mfMinus)
    )  

  ///Absolute Price Osc
  let apoLookback (ma:MA) slowPeriod =
    ma.lookback slowPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Absolute Price Oscillator");
    Description("Returns the absolute price oscillator");
  >]
  let apo (ma:MA) fastPeriod slowPeriod (data:float[]) =
    Study.checkPositiveIntMin1 fastPeriod
    if fastPeriod > slowPeriod then
      invalidArg "fastPeriod" "fast period > slow period"
    let lookbackTotal = apoLookback ma slowPeriod
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let fastMA = ma.create fastPeriod data
      let slowMA = ma.create slowPeriod data
      let offset = fastMA.Length - slowMA.Length
      Array.init slowMA.Length (fun i -> fastMA.[i + offset] - slowMA.[i])
    )

  ///Percentage Price Osc
  let ppoLookback (ma:MA) slowPeriod =
    ma.lookback slowPeriod

  [<TradingStudy;
    Group("Cycle");
    Title("Percentage Price Oscillator");
    Description("Returns the percentage price oscillator");
  >]
  let ppo (ma:MA) fastPeriod slowPeriod (data:float[]) =
    Study.checkPositiveIntMin1 fastPeriod
    if fastPeriod > slowPeriod then
      invalidArg "fastPeriod" "fast period > slow period"
    let lookbackTotal = apoLookback ma slowPeriod
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let fastMA = ma.create fastPeriod data
      let slowMA = ma.create slowPeriod data
      let offset = fastMA.Length - slowMA.Length
      Array.init slowMA.Length (fun i -> 100.0 * (fastMA.[i + offset] / slowMA.[i] - 1.0))
    )

  ///Vertical Horizon Filter
  let vhfLookback period =
    Math.sumLookback period + rocLookback 1

  [<TradingStudy;
    Group("Cycle");
    Title("Vertical Horizon Filter");
    Description("Returns the vertical horizon filter");
  >]
  let vhf period (h:float[]) l c =
    Study.checkPositiveIntMin1 period
    Study.checkHighLowClose h l c
    let lookbackTotal = vhfLookback period
    let startIdx = lookbackTotal
    let endIdx = h.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let highs = Stat.max period h
      let lows = Stat.min period l
      let rocSum = roc true 0.0 1 c |> Math.sum period
      let offset = highs.Length - rocSum.Length
      Array.init rocSum.Length (fun i ->
        (highs.[i+offset] - lows.[i+offset]) / rocSum.[i]
      )
    )

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.

Moving Averages

namespace Trading.Studies

type MA =
  | Simple
  | Wilder
  | Exponential
  | DoubleExponential
  | TripleExponential
  | Tillson3 of float
  | Triangular
  | Weighted
  | KaufmanAdaptive
  | ZeroLag of float
  | VolumeWeighted of float[]
  | Hull
  | SineWeighted 

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module MA =

  let smaLookback period = Stat.meanLookback period

  [<TradingStudy;
    Group("Smoothing");
    Title("Simple MA");
    Description("Returns the simple moving average");
    Overlay;
  >]
  let sma period data = Stat.mean period data

  let baseEmaLookback period = period - 1

  let baseEMA period coeff (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = baseEmaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let n = float period

      for i in startIdx - lookbackTotal .. startIdx do
        out.[0] <- out.[0] + (data.[i] / n)

      for i in startIdx+1 .. endIdx do
        let outIdx = i - startIdx
        let prev = out.[outIdx-1]
        out.[outIdx] <- prev + coeff * (data.[i] - prev)

      out
    )

  let wilmaLookback period = baseEmaLookback period

  [<TradingStudy;
    Group("Smoothing");
    Title("Wilder MA");
    Description("Returns the Wilder moving average");
    Overlay;
  >]
  let wilma period data =
    let coeff = 1.0 / float period
    baseEMA period coeff data

  let emaLookback period = baseEmaLookback period

  [<TradingStudy;
    Group("Smoothing");
    Title("Exponential MA");
    Description("Returns the exponential moving average");
    Overlay;
  >]
  let ema period data =
    let coeff = 2.0 / (1.0 + float period)
    baseEMA period coeff data    

  let demaLookback period = 2 * (emaLookback period)

  [<TradingStudy;
    Group("Smoothing");
    Title("Double Exponential MA");
    Description("Returns the double exponential moving average");
    Overlay;
  >]
  let dema period (data:float[]) =
    let lookbackTotal = baseEmaLookback period
    let startIdx = demaLookback period
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let ema1 = ema period data
      let ema2 = ema period ema1
      let offset = ema1.Length - ema2.Length
      Array.init ema2.Length (fun i -> 2.0 * ema1.[i + offset] - ema2.[i])
    ) 

  let temaLookback period = 3 * (emaLookback period)

  [<TradingStudy;
    Group("Smoothing");
    Title("Triple Exponential MA");
    Description("Returns the triple exponential moving average");
    Overlay;
  >]
  let tema period (data:float[]) =
    let lookbackTotal = baseEmaLookback period
    let startIdx = temaLookback period
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let ema1 = ema period data
      let ema2 = ema period ema1
      let ema3 = ema period ema2
      let o13 = ema1.Length - ema3.Length
      let o23 = ema2.Length - ema3.Length
      Array.init ema3.Length (fun i ->
        3.0 * ema1.[i + o13] - 3.0 * ema2.[i + o23] + ema3.[i]
      )
    )

  (* Tim Tillson - TechnicalAnalysis of Stocks and Commodities - January 1998 *)
  let t3Lookback period = 6 * (emaLookback period)

  [<TradingStudy;
    Group("Smoothing");
    Title("Tillson's T3");
    Description("Returns Tillson's T3 - TechnicalAnalysis of Stocks and Commodities (Jan. 1998)");
    Overlay;
  >]
  let t3 period volumeFactor (data:float[]) =
    let lookbackTotal = baseEmaLookback period
    let startIdx = t3Lookback period
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let ema1 = ema period data
      let ema2 = ema period ema1
      let ema3 = ema period ema2
      let ema4 = ema period ema3
      let ema5 = ema period ema4
      let ema6 = ema period ema5
      //constants
      let vf2 = volumeFactor * volumeFactor
      let vf3 = vf2 * volumeFactor
      let c1 = -vf3
      let c2 = 3.0 * (vf2 + vf3)
      let c3 = -6.0*vf2 - 3.0*(volumeFactor + vf3)
      let c4 = 1.0 + 3.0*volumeFactor + vf3 + 3.0*vf2
      //offsets
      let lookbackOneEma = emaLookback period
      let o36 = 3 * lookbackOneEma
      let o46 = 2 * lookbackOneEma
      let o56 = lookbackOneEma
      //output
      Array.init ema6.Length (fun i ->
        c1*ema6.[i] + c2*ema5.[i+o56] + c3*ema4.[i+o46] + c4*ema3.[i+o36]
      )
    )

  (* A triangular SMA is an SMA of SMA whose period are computed as follow :
      if period = even -> SMA(period/2 + 1, SMA(period/2, data))
      if period = odd -> SMA((period+1)/2, SMA((period+1)/2 , data))
  *)
  let trimaLookback period = smaLookback period

  [<TradingStudy;
    Group("Smoothing");
    Title("Triangular MA");
    Description("Returns the triangular moving average");
    Overlay;
  >]
  let trima period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = trimaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let period1, period2 =
        if period % 2 = 0 then
          let tmp = period / 2
          tmp, tmp + 1
        else
          let tmp = (period+1) / 2
          tmp, tmp  

      sma period1 data |> sma period2
    )

  let wmaLookback period = period - 1

  [<TradingStudy;
    Group("Smoothing");
    Title("Weighted MA");
    Description("Returns the weighted moving average");
    Overlay;
  >]
  let wma period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = wmaLookback 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 coeff = n * (n + 1.0) / 2.0
      let mutable substract = 0.0
      let mutable acc = 0.0

      let mutable weight = 1.0
      for i in startIdx - lookbackTotal .. startIdx do
        acc <- acc + data.[i] * weight
        weight <- weight + 1.0
        substract <- substract + data.[i]

      out.[0] <- acc / coeff

      for i in startIdx + 1 .. endIdx do
        let outIdx = i - startIdx
        acc <- acc + (n * data.[i]) - substract
        substract <- substract + data.[i] - data.[i-period]
        out.[outIdx] <- acc / coeff

      out
    )

  (*  Perry Kaufman - Stocks & Commodities magazine - March, 1995
      Note that contrary to most moving averages, even if period = 1, the output
      differs from the input. This is because "period" doesn't refer to the number
      of values serving as an input to construct an output, but it corresponds
      to an index offset, in the same way as it does for the Rate of change or momentum
      indicators
  *)
  let kamaLookback period =
    Math.sumLookback period + BaseStudies.momLookback 1

  [<TradingStudy;
    Group("Smoothing");
    Title("Kaufman Adaptive MA");
    Description("Returns the Kaufman adaptive moving average");
    Overlay;
  >]
  let kama period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = kamaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let direction = BaseStudies.mom period data

      let volatility =
        BaseStudies.mom 1 data |> Array.map abs |> Math.sum period

      let slow = 2.0 / 31.0
      let diff = (2.0/3.0) - slow

      let smoothAux i currentVolatility =
        let efficiencyRatio =
          let dir = direction.[i]
          if currentVolatility <> 0.0 then abs <| dir / currentVolatility else 1.0
        (slow + diff * efficiencyRatio) ** 2.0

      let smooth = Array.mapi smoothAux volatility

      let mutable prev = data.[startIdx-1]
      out.[0] <- prev + smooth.[0] * (data.[startIdx] - prev)

      for today in startIdx + 1 .. endIdx do
        let outIdx = today - startIdx
        prev <- out.[outIdx - 1]
        out.[outIdx] <- prev + smooth.[outIdx] * (data.[today] - prev)

      out
    )

  //==========================================
  //
  // NOT CHECKED
  //
  //==========================================  

  let zlmaLag period = (period - 1) / 2

  let zlmaLookback period = emaLookback period + zlmaLag period

  [<TradingStudy;
    Group("Smoothing");
    Title("Zero-Lag MA");
    Description("Returns the zero-lag moving average");
    Overlay;
  >]
  let zlma k period data =
    data
      |> BaseStudies.wmom (1.0+k) k (zlmaLag period)
      |> ema period

  let vwmaLookback period = period - 1

  [<TradingStudy;
    Group("Smoothing");
    Title("Volume-Weighted MA");
    Description("Returns the volume-weighted moving average");
    Overlay;
  >]
  let vwma period volume (data:float[]) =
    Study.checkPositiveIntMin1 period
    Study.checkVolume volume
    let lookbackTotal = vwmaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let mutable sum = 0.0
      let mutable sumProd = 0.0

      for i in startIdx - lookbackTotal .. startIdx do
        sum <- sum + volume.[i]
        sumProd <- sumProd + (data.[i] * volume.[i])

      out.[0] <- sumProd / sum

      let trailingStartIdx = startIdx - lookbackTotal
      for i in startIdx + 1 .. endIdx do
        let outIdx = i - startIdx
        let trailingIdx = i - (lookbackTotal+1)
        sum <- sum + volume.[i] - volume.[trailingIdx]
        sumProd <- sumProd + (data.[i] * volume.[i]) - (data.[trailingIdx] * volume.[trailingIdx])
        out.[outIdx] <- sumProd / sum

      out
    )

  let hmaSquareRoot = float >> sqrt >> int

  let hmaLookbackSquareRoot period =
    wmaLookback (hmaSquareRoot period)

  let hmaLookback period =
    wmaLookback period + hmaLookbackSquareRoot period

  [<TradingStudy;
    Group("Smoothing");
    Title("Hull MA");
    Description("Returns the Hull moving average");
    Overlay;
  >]
  let hma period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = hmaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let wma1 = wma (period/2) data
      let wma2 = wma period data
      let offset = wma1.Length - wma2.Length
      let deltas = Array.init wma2.Length (fun i -> 2.0 * wma1.[i+offset] - wma2.[i])
      wma (hmaSquareRoot period) deltas
    )

  let swmaLookback period = period - 1

  let swmaWeights period =
    let n = float period
    Array.init period (fun i -> (float (i+1) * System.Math.PI) / n |> sin)

  [<TradingStudy;
    Group("Smoothing");
    Title("Sine-Weighted MA");
    Description("Returns the sine-weighted moving average");
    Overlay;
  >]
  let swma period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = vwmaLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let ws = swmaWeights period
      let denom = Array.reduce (+) ws
      Array.init outLen (fun i ->
        let mutable sum = 0.0
        let today = startIdx + i
        for weightIdx in 0 .. period - 1 do
          sum <- sum + ws.[weightIdx] * data.[today - weightIdx]
        sum / denom
      )
    )

type MA
  with
    member x.create p =
      match x with
      | Simple -> MA.sma p
      | Wilder -> MA.wilma p
      | Exponential -> MA.ema p
      | DoubleExponential -> MA.dema p
      | TripleExponential -> MA.trima p
      | Tillson3 volumeFactor -> MA.t3 p volumeFactor
      | Triangular -> MA.trima p
      | Weighted -> MA.wma p
      | KaufmanAdaptive -> MA.kama p
      | ZeroLag k -> MA.zlma k p
      | VolumeWeighted volumeData -> MA.vwma p volumeData
      | Hull -> MA.hma p
      | SineWeighted -> MA.swma p 

    member x.lookback p =
      match x with
      | Simple -> MA.smaLookback p
      | Wilder -> MA.wilmaLookback p
      | Exponential -> MA.emaLookback p
      | DoubleExponential -> MA.demaLookback p
      | TripleExponential -> MA.trimaLookback p
      | Tillson3 volumeFactor -> MA.t3Lookback p
      | Triangular -> MA.trimaLookback p
      | Weighted -> MA.wmaLookback p
      | KaufmanAdaptive -> MA.kamaLookback p
      | ZeroLag _ -> MA.zlmaLookback p
      | VolumeWeighted volumeData -> MA.vwmaLookback p
      | Hull -> MA.hmaLookback p
      | SineWeighted -> MA.swmaLookback p 

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

Some base studies we’ll use in other studies

namespace Trading.Studies

module internal BaseStudies =

  let wmomLookback (period:int) = period

  let wmom coeffToday coeffLag period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = wmomLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let f =
        match coeffToday, coeffLag with
        | 0.0, 0.0 -> fun today -> 0.0
        | 0.0, 1.0 -> fun today -> -data.[today - lookbackTotal]
        | 0.0, beta -> fun today -> -beta * data.[today - lookbackTotal]
        | 1.0, 0.0 -> fun today -> data.[today]
        | 1.0, 1.0 -> fun today -> data.[today] - data.[today - lookbackTotal]
        | alpha, 0.0 -> fun today -> alpha * data.[today]
        | alpha, beta -> fun today -> alpha * data.[today] - beta * data.[today - lookbackTotal]

      Array.init outLen (fun i -> f (startIdx + i))
    )

  let momLookback period = wmomLookback period

  let mom period data = wmom  1.0 1.0 period data

  let rocLookback (period:int) = period

  let roc isBase100 divByZeroDefaultValue period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = wmomLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      if isBase100 then
        Array.init outLen (fun i ->
          let today = startIdx + i
          let lagValue = today - period
          if data.[lagValue] <> 0.0 then
            100.0 * (data.[today] / data.[lagValue] - 1.0)
          else
            divByZeroDefaultValue
        )
      else
        Array.init outLen (fun i ->
          let today = startIdx + i
          let lagValue = today - period
          if data.[lagValue] <> 0.0 then
            data.[today] / data.[lagValue] - 1.0
          else
            divByZeroDefaultValue
        )
    )

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.

Price transforms

namespace Trading.Studies

module Price =

  let medLookback = 0

  [<TradingStudy;
    Group("Price");
    Title("Median Price");
    Description("Returns (High + Low) / 2");
    Overlay;
  >]
  let med high low =
    Study.checkHighLow high low
    let lookbackTotal = medLookback
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx+i
        (high.[today] + low.[today]) / 2.0
      )
    )

  //=====================================================

  let typLookback = 0

  [<TradingStudy;
    Group("Price");
    Title("Typical Price");
    Description("Returns (High + Low + Close) / 3");
    Overlay;
  >]
  let typ high low close =
    Study.checkHighLowClose high low close
    let lookbackTotal = typLookback
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx+i
        (high.[today] + low.[today] + close.[today]) / 3.0
      )
    )

  //=====================================================       

  let avgLookback = 0

  [<TradingStudy;
    Group("Price");
    Title("Average Price");
    Description("Returns (Open + High + Low + Close) / 4");
    Overlay;
  >]
  let avg o high low close =
    Study.checkOpenHighLowClose o high low close
    let lookbackTotal = avgLookback
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx+i
        (o.[today] + high.[today] + low.[today] + close.[today]) / 4.0
      )
    )

  //=====================================================       

  let wclLookback = 0

  [<TradingStudy;
    Group("Price");
    Title("Weighted Close Price");
    Description("Returns (High + Low + 2xClose) / 4");
    Overlay;
  >]
  let wcl high low close =
    Study.checkHighLowClose high low close
    let lookbackTotal = wclLookback
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx+i
        (high.[today] + low.[today] + 2.0 * close.[today]) / 4.0
      )
    )

  //=====================================================

  let midLookback period = Stat.maxLookback period

  [<TradingStudy;
    Group("Price");
    Title("Mid Price");
    Description("Returns (Highest high + Lowest low) / 2");
    Overlay;
  >]
  let mid period high low =
    Study.checkPositiveIntMin1 period
    Study.checkHighLow high low
    let lookbackTotal = midLookback period
    let startIdx = lookbackTotal
    let endIdx = high.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let hh = Stat.max period high
      let ll = Stat.min period low
      Array.map2 (fun x y -> (x+y) / 2.0) hh ll
    )

  //=====================================================

  let midPointLookback period = midLookback period

  [<TradingStudy;
    Group("Price");
    Title("Midpoint Price");
    Description("Same as mid price : returns (Highest high + Lowest low) / 2");
    Overlay;
  >]
  let midPoint period data =
    mid period data data

We show how to create a PriceSeries object by downloading prices from Yahoo! Finance while giving the basis for further data sources developments.

PriceSeries.fs

namespace Trading.Data

open System

type PriceSeries =
    { Date : DateTime[]
      Open : float[]
      High : float[]
      Low : float[]
      Close : float[]
      Volume : float[]
    }
      static member create n =
        { Date = Array.zeroCreate n
          Open = Array.zeroCreate n
          High = Array.zeroCreate n
          Low = Array.zeroCreate n
          Close = Array.zeroCreate n
          Volume = Array.zeroCreate n
        }

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module PriceSeries =

  let create n =
    { Date = Array.zeroCreate n
      Open = Array.zeroCreate n
      High = Array.zeroCreate n
      Low = Array.zeroCreate n
      Close = Array.zeroCreate n
      Volume = Array.zeroCreate n
    }

  let rec private areDatesInvalidAux dates i found =
    if found then true
    elif i >= Array.length dates then found
    else
      areDatesInvalidAux dates (i+1) (dates.[i] < dates.[i-1])

  let checkDates dates =
    if Array.length dates > 1 && areDatesInvalidAux dates 1 false then
      invalidArg "dates" "dates are not sorted"

  let checkSameLength a b =
    if Array.length a <> Array.length b then
      invalidArg "input arrays" "input arrays have different lengths"

  let checkHighLow high low =
    checkSameLength high low
    for i in 0 .. high.Length - 1 do
      if high.[i] < low.[i] then
        failwithf "high < low at index %d" i

  let checkHighLowClose high low c =
    checkSameLength high low
    checkSameLength high c
    for i in 0 .. Array.length high - 1 do
      if c.[i] > high.[i] then
        failwithf "open > high at index %d" i
      if low.[i] > c.[i] then
        failwithf "low > open at index %d" i

  let checkOpenHighLowClose o high low c =
    checkSameLength high low
    checkSameLength high o
    checkSameLength high c
    for i in 0 .. Array.length high - 1 do
      if o.[i] > high.[i] then
        failwithf "open > high at index %d" i
      if low.[i] > o.[i] then
        failwithf "low > open at index %d" i
      if c.[i] > high.[i] then
        failwithf "close > high at index %d" i
      if low.[i] > c.[i] then
        failwithf "low > close at index %d" i

DataLoader.fs

For the Excel DataLoader, we use the FileHelpers library by Marcos Meli.

namespace Trading.Data

open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions

open FileHelpers
open FileHelpers.DataLink

open Trading.Data

type DataLoader =
  interface
    abstract AsyncDownload : string -> DateTime -> DateTime -> Async<PriceSeries>
  end

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module DataLoader =

  module Excel =

    [<DelimitedRecord("|")>]
    type PriceSample () =
      [<DefaultValue>]
      val mutable Date : DateTime
      [<DefaultValue>]
      val mutable Open : float
      [<DefaultValue>]
      val mutable High : float
      [<DefaultValue>]
      val mutable Low : float
      [<DefaultValue>]
      val mutable Close : float
      [<DefaultValue>]
      val mutable Volume : float

    let create tickerDir ext startRow startColumn =
      {new DataLoader with
        override x.AsyncDownload ticker first last =
          async {
            let fullPath = System.IO.Path.Combine(tickerDir, ticker + ext)
            let provider =
              new ExcelStorage(
                typeof<PriceSample>,
                StartRow=startRow,
                StartColumn=startColumn,
                FileName=fullPath
              )
            let res =
              provider.ExtractRecords()
                |> unbox<PriceSample[]>
                |> Array.filter (fun sample -> sample.Date >= first && sample.Date <= last)
                |> Array.sortBy (fun sample -> sample.Date)

            let series = PriceSeries.create res.Length

            res |> Array.iteri (fun i sample ->
              series.Date.[i] <- sample.Date
              series.Open.[i] <- sample.Open
              series.High.[i] <- sample.High
              series.Low.[i] <- sample.Low
              series.Close.[i] <- sample.Close
              series.Volume.[i] <- sample.Volume
            )

            PriceSeries.checkDates series.Date
            PriceSeries.checkOpenHighLowClose series.Open series.High series.Low series.Close

            return series
          }
      }

  module Yahoo =

    let createUrl ticker (first:DateTime) (last:DateTime) =
        sprintf
          @"http://ichart.finance.yahoo.com/table.csv?s=%s&a=%d&b=%d&c=%d&d=%d&e=%d&f=%d&g=d&ignore=.csv"
            ticker
            (first.Month - 1) first.Day first.Year
            (last.Month - 1) last.Day last.Year 

    let asyncDownload url =
      async {
        let client = new WebClient()
        return! client.AsyncDownloadString(url)
      }

    let create () =
      {new DataLoader with
        override x.AsyncDownload ticker first last =
          async {
            let url =  Uri(createUrl ticker first last)
            let! data = asyncDownload url
            let lines = data.Split( [|'\n'|]).[1..]
            let series =  PriceSeries.create lines.Length
            let firstOk = ref -1
            let counter = ref 0
            for line in 0 .. lines.Length - 1 do
              let columns = lines.[line].Split([|','|])
              if Array.length columns = 7 then
                let date = DateTime.Parse columns.[0]
                if date >= first && date <= last then
                  if !firstOk < 0 then firstOk := line
                  incr counter
                  series.Date.[line] <- date
                  series.Open.[line] <- float columns.[1]
                  series.High.[line] <- float columns.[2]
                  series.Low.[line] <- float columns.[3]
                  //adjusted close
                  //normal close would be the 4th column
                  series.Close.[line] <- float columns.[6]
                  series.Volume.[line] <- float columns.[5]
            //Delete invalid dates
            if !counter <> lines.Length then
              let newSeries =  PriceSeries.create !counter
              Array.blit series.Date !firstOk newSeries.Date 0 !counter
              Array.blit series.Open !firstOk newSeries.Open 0 !counter
              Array.blit series.High !firstOk newSeries.High 0 !counter
              Array.blit series.Low !firstOk newSeries.Low 0 !counter
              Array.blit series.Close !firstOk newSeries.Close 0 !counter
              Array.blit series.Volume !firstOk newSeries.Volume 0 !counter
              return newSeries
            else
              return series
          }
      }

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.

Math and statistics-related indicators

namespace Trading.Studies

module Stat =

  open Trading.Studies

  let cmpLookback period = period - 1

  let rec movingCompareAux today endIdx extremaPosition cmpF (data:float[]) =
    if today > endIdx then extremaPosition
    else
      let newPosition =
        if cmpF data.[today] data.[extremaPosition] then
          today
        else
          extremaPosition
      movingCompareAux (today+1) endIdx newPosition cmpF data

  let cmp cmpF period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = cmpLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let today = startIdx - lookbackTotal
      out.[0] <- movingCompareAux today startIdx today cmpF data

      for today in startIdx + 1 .. endIdx do
        let outIdx = today - startIdx
        let prevIdx = out.[outIdx - 1]
        let current =
          //the last extrema is out of the observation range : scan the whole range
          if prevIdx <= today - period then
            movingCompareAux (today-lookbackTotal) today today cmpF data
          //the last extrema is in the observation range : compare with the latest observation
          else
            if cmpF data.[today] data.[prevIdx] then today else prevIdx
        out.[outIdx] <- current

      out
    )

  //=====================================================

  let minIdxLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Minimum index");
    Description("Returns the position of the minimum value within the last n observations");
  >]
  let minIdx period data =
    cmp (<=) period data 

  //=====================================================

  let maxIdxLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Maximum index");
    Description("Returns the position of the maximum value within the last n observations");
  >]
  let maxIdx period data =
    cmp (>=) period data 

  //=====================================================

  let minLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Minimum");
    Description("Returns the minimum value within the last n observations");
    Overlay;
  >]
  let min ([<DefaultValue("14"); Numeric("1","nan","1")>]period) data =
    minIdx period data |> Array.map (Array.get data)

  //=====================================================

  let maxLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Maximum");
    Description("Returns the minimum value within the last n observations");
    Overlay;
  >]
  let max ([<DefaultValue("14"); Numeric("1","nan","1")>]period) data =
    maxIdx period data |> Array.map (Array.get data)

  //=====================================================

  let meanLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Mean");
    Description("Returns the mean value of the last n observations");
    Overlay;
  >]
  let mean period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = meanLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let n = float period

      for i in startIdx - lookbackTotal .. startIdx do
        out.[0] <- out.[0] + (data.[i] / n)

      for i in startIdx+1 .. endIdx do
        let outIdx = i - startIdx
        out.[outIdx] <- out.[outIdx-1] + ((data.[i] - data.[i-period])/ n)

      out
    )

  //=====================================================

  let medianLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Median");
    Description("Returns the median value of the last n observations");
    Overlay;
  >]
  let median period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = medianLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      let buffer = Array.zeroCreate period
      let n = float period

      for today in startIdx - lookbackTotal .. startIdx - 1 do
        buffer.[Study.circularIndex today buffer] <- data.[today]

      for today in startIdx .. endIdx do
        let outIdx = today - startIdx
        buffer.[Study.circularIndex today buffer] <- data.[today]
        out.[outIdx] <- Study.median buffer

      out
    )

  //=====================================================

  let avgDevLookback period = period - 1

  let rec avgDevAux today stop currentMean acc n (data:float[]) =
    if today <= stop then
      let toAdd = abs (data.[today] - currentMean) / n
      avgDevAux (today+1) stop currentMean (acc + toAdd) n data
    else acc

  [<TradingStudy;
    Group("Statistics");
    Title("Average Absolute Deviation");
    Description("Returns theaverage absolute deviation of the last n observations");
  >]
  let avgDev period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = avgDevLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let n = float period
      let trailingIdx = startIdx - lookbackTotal
      data
        |> mean period
        |> Array.mapi (fun i currentMean ->
            let currentTrailingIdx = trailingIdx + i
            avgDevAux currentTrailingIdx (currentTrailingIdx+lookbackTotal) currentMean 0.0 n data
          )
    )

  //=====================================================

  let varLookback period = period - 1

  [<TradingStudy;
    Group("Variance");
    Title("Average Absolute Deviation");
    Description("Returns the variance of the last n observations");
  >]
  let var isSample period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = varLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let meanX = data |> mean period
      let xSquared = Array.map (fun x -> x * x) data
      let meanXSquared = xSquared |> mean period
      if not isSample then
        Array.map2 (fun mxx mx -> mxx - (mx * mx)) meanXSquared meanX
      else
        let n = float period
        let coeff = n / (n - 1.0)
        Array.map2 (fun mxx mx -> coeff * (mxx - (mx * mx))) meanXSquared meanX
    )

  //=====================================================

  let stDevLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Standard Deviation");
    Description("Returns the standard deviation of the last n observations");
  >]
  let stDev isSample period data =
    data
      |> var isSample period
      |> Array.map sqrt

  //=====================================================

  let stErrLookback period = period - 1

  [<TradingStudy;
    Group("Statistics");
    Title("Standard Error");
    Description("Returns the standard error of the last n observations");
  >]
  let stErr period data =
    data
      |> stDev true period
      |> Array.map (
              let p = float period
              let x = sqrt p |> ref
              //Finite population correctino when sample >= 5% population
              let n = Array.length data |> float
              if p >= 0.05 * n then
                x := !x * sqrt ((n-p)/(n-1.0))
              fun std -> std / !x
          )

  //=====================================================

  let skewLookback period = period - 1

  let rec skewAux today stop currentMean currentStDev acc coeff (data:float[]) =
    if today <= stop then
      let toAdd = (data.[today] - currentMean) / currentStDev
      skewAux (today+1) stop currentMean currentStDev (acc + (toAdd**3.0)) coeff data
    else coeff * acc

  [<TradingStudy;
    Group("Statistics");
    Title("Skew");
    Description("Returns the skew of the last n observations");
  >]
  let skew period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = varLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let n = float period
      let coeff = n / ((n - 1.0) * (n - 2.0))
      let trailingIdx = startIdx - lookbackTotal

      let mean = data |> mean period
      let sd = data |> stDev true period

      Array.mapi2 (fun i m s ->
        let tIdx = trailingIdx + i
        data |> skewAux tIdx (tIdx+lookbackTotal) m s 0.0 coeff
      ) mean sd
    )

  //=====================================================

  let kurtLookback period = period - 1

  let rec kurtAux today stop currentMean currentStDev acc coeff toSubstract (data:float[]) =
    if today <= stop then
      let toAdd = (data.[today] - currentMean) / currentStDev
      kurtAux (today+1) stop currentMean currentStDev (acc + (toAdd**4.0)) coeff toSubstract data
    else (coeff * acc) - toSubstract

  [<TradingStudy;
    Group("Statistics");
    Title("Kurtosis");
    Description("Returns the kurtosis of the last n observations");
  >]
  let kurt period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = varLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let n = float period
      let n1 = float lookbackTotal
      let n23 = (n - 2.0) * (n - 3.0)
      let coeff = (n * (n + 1.0)) / (n1 * n23)
      let toSubstract = 3.0 * (n1 * n1) / n23
      let trailingIdx = startIdx - lookbackTotal

      let mean = data |> mean period
      let sd = data |> stDev true period

      Array.mapi2 (fun i m s ->
        let tIdx = trailingIdx + i
        data |> kurtAux tIdx (tIdx+lookbackTotal) m s 0.0 coeff toSubstract
      ) mean sd
    )

  //=====================================================

  let covarLookback period = Math.sumLookback period

  [<TradingStudy;
    Group("Statistics");
    Title("Covariance");
    Description("Returns the covariance of two series over the last n observations");
    MultipleInputSeriesAttribute;
  >]
  let covar period (data1:float[]) data2 =
    Study.checkPositiveIntMin1 period
    Study.checkSameInputLength [data1; data2]
    let lookbackTotal = varLookback period
    let startIdx = lookbackTotal
    let endIdx = data1.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let xsum = data1 |> Math.sum period
      let ysum = data2 |> Math.sum period
      let xySum = Math.mult data1 data2 |> Math.sum period
      let n = float period
      Array.init xySum.Length (fun i -> (xySum.[i] - (xsum.[i] * ysum.[i]) / n) / n)
    )

  //=====================================================

  let correlLookback period = Math.sumLookback period

  [<TradingStudy;
    Group("Statistics");
    Title("Correlation coefficient");
    Description("Returns the correlation coefficient of two series over the last n observations");
    MultipleInputSeriesAttribute;
  >]
  let correl isSample period valueIfDenomZero (data1:float[]) data2 =
    Study.checkPositiveIntMin1 period
    Study.checkSameInputLength [data1; data2]
    let lookbackTotal = varLookback period
    let startIdx = lookbackTotal
    let endIdx = data1.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let xy = covar period data1 data2
      let x = stDev isSample period data1
      let y = stDev isSample period data2
      Array.init xy.Length (fun i ->
        if x.[i] <> 0.0 && y.[i] <> 0.0 then xy.[i] / (x.[i] * y.[i]) else valueIfDenomZero
      )
    )

  //=====================================================

  let linRegLookback period = covarLookback period

  let linRegXs n = Array.init n float

  [<TradingStudy;
    Group("Statistics");
    Title("Linear Regression Slope");
    Description("Returns the slope of the linear regression of the last n observations");
  >]
  let linRegSlope isSample period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = linRegLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let xs = linRegXs data.Length
      let cov = covar period xs data
      let var = var isSample period xs
      Array.map2 ( / ) cov var
    )

  [<TradingStudy;
    Group("Statistics");
    Title("Linear Regression Intercept");
    Description("Returns the intercept of the linear regression of the last n observations");
  >]
  let linRegIntercept isSample period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = linRegLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let xs = linRegXs data.Length
      let xmean = mean period xs
      let ymean = mean period data
      let slopes = linRegSlope isSample period data
      Array.init slopes.Length (fun i -> ymean.[i] - (slopes.[i] * xmean.[i]))
    )   

  [<TradingStudy;
    Group("Statistics");
    Title("Linear Regression");
    Description("Returns the linear regression of the last n observations");
    Overlay;
  >]
  let linReg isSample period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = linRegLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let xs = linRegXs data.Length
      let slopes = linRegSlope isSample period data
      let intercepts = linRegIntercept isSample period data
      Array.init slopes.Length (fun i -> intercepts.[i] + (slopes.[i] * xs.[i + lookbackTotal]))
    )

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

Math

namespace Trading.Studies

module Math =

  open Trading.Studies

  let sumLookback period = period - 1

  [<TradingStudy;
    Group("Math");
    Title("Summation");
    Description("Returns the sum of the last n observations")
  >]
  let sum period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = sumLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      for i in startIdx - lookbackTotal .. startIdx do
        out.[0] <- out.[0] + data.[i]

      for today in startIdx + 1 .. endIdx do
        let outIdx = today - startIdx
        out.[outIdx] <- out.[outIdx - 1] + data.[today] - data.[today - period]

      out
    )

  //=====================================================

  let prodLookback period = period - 1

  let rec productAux today stop acc (data:float[]) =
    if acc = 0.0 then 0.0
    elif today <= stop then
      productAux (today+1) stop (data.[today] *acc) data
    else acc

  [<TradingStudy;
    Group("Math");
    Title("Product");
    Description("Returns the product of the last n observations")
  >]
  let prod period (data:float[]) =
    Study.checkPositiveIntMin1 period
    let lookbackTotal = prodLookback period
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let out = Array.zeroCreate outLen

      out.[0] <- data |> productAux (startIdx - lookbackTotal) startIdx 1.0

      for today in startIdx + 1 .. endIdx do
        let outIdx = today - startIdx
        let outNew =
          match data.[today - period], out.[outIdx - 1] with
          | 0.0, _ -> data |> productAux (today - lookbackTotal) today 1.0
          | _, 0.0 -> 0.0
          | dataPrev,  outPrev -> outPrev / dataPrev * data.[today]
        out.[outIdx] <- outNew

      out
    )

  //=====================================================

  let scaleLookback = 0

  [<TradingStudy;
    Group("Math");
    Title("Scale");
    Description("Returns the observations multiplied by a coefficient")
  >]
  let scale weight (data:float[]) =
    let lookbackTotal = scaleLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let f =
        match weight with
        | 0.0 -> fun idx -> 0.0
        | 1.0 -> fun idx -> data.[idx]
        | w -> fun idx -> w * data.[idx]

      Array.init outLen f
    )

  //=====================================================

  let shiftLookback = 0

  [<TradingStudy;
    Group("Math");
    Title("Scale");
    Description("Returns the observations shifted by a coefficient")
  >]
  let shift delta (data:float[]) =
    let lookbackTotal = shiftLookback
    let startIdx = lookbackTotal
    let endIdx = data.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let f =
        match delta with
        | 0.0 -> fun idx -> data.[idx]
        | x -> fun idx -> x + data.[idx]
      Array.init outLen f
    )

  //=====================================================

  let waddLookback = 0

  [<TradingStudy;
    Group("Math");
    Title("Weighted addition");
    Description("Returns the weighted sum of two series");
    MultipleInputSeriesAttribute;
  >]
  let wadd weight1 weight2 (data1:float[]) data2 =
    Study.checkSameInputLength [data1; data2]
    let lookbackTotal = waddLookback
    let startIdx = lookbackTotal
    let endIdx = data1.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      let f =
        match weight1, weight2 with
        | 0.0, 0.0 -> fun idx -> 0.0
        | 0.0, 1.0 -> fun idx -> data2.[idx]
        | 0.0, beta -> fun idx -> beta * data2.[idx]
        | 1.0, 0.0 -> fun idx -> data1.[idx]
        | alpha, 0.0 -> fun idx -> alpha * data1.[idx]
        | 1.0, 1.0 -> fun idx -> data1.[idx] + data2.[idx]
        | alpha, beta -> fun idx -> alpha * data1.[idx] + beta * data2.[idx]

      Array.init outLen (fun i -> f (startIdx + i))
    )

  //=====================================================

  let addLookback = waddLookback

  [<TradingStudy;
    Group("Math");
    Title("Addition");
    Description("Returns the sum of two series");
    MultipleInputSeriesAttribute;
  >]
  let add data1 data2 =
    wadd 1.0 1.0 data1 data2

  //=====================================================

  let subLookback = waddLookback

  [<TradingStudy;
    Group("Math");
    Title("Substraction");
    Description("Returns the difference between two series");
    MultipleInputSeriesAttribute;
  >]
  let sub data1 data2 =
    wadd 1.0 (-1.0) data1 data2

  //=====================================================

  let multLookback = 0

  [<TradingStudy;
    Group("Math");
    Title("Multiplication");
    Description("Returns the product of two series");
    MultipleInputSeriesAttribute;
  >]
  let mult (data1:float[]) data2 =
    Study.checkSameInputLength [data1; data2]
    let lookbackTotal = waddLookback
    let startIdx = lookbackTotal
    let endIdx = data1.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx + i
        data1.[today] * data2.[today]
      )
    )

  //=====================================================

  let divLookback = 0

  [<TradingStudy;
    Group("Math");
    Title("Division");
    Description("Returns the ratio of two series");
    MultipleInputSeriesAttribute;
  >]
  let div valueIfDenominatorIsZero (data1:float[]) data2 =
    Study.checkSameInputLength [data1; data2]
    let lookbackTotal = waddLookback
    let startIdx = lookbackTotal
    let endIdx = data1.Length - 1
    Study.lazyCompute startIdx endIdx (fun outLen ->
      Array.init outLen (fun i ->
        let today = startIdx + i
        if data2.[today] <> 0.0 then data1.[today] / data2.[today] else valueIfDenominatorIsZero
      )
    )

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.

Abstract : type definitions and helpers

We use attributes to describe the functions so that Reflection can help us generate information about them.

namespace Trading.Studies

exception BadParam of string

open System
open System.Reflection
open Microsoft.FSharp.Reflection

//
//  General flags
//

///Indicates the function shall be marked as a study.
type TradingStudyAttribute() =
  inherit System.Attribute()

///Group of studies the study belongs to.
type GroupAttribute(group:string) =
  inherit System.Attribute()
  member self.Group = group

///Title of the study.
type TitleAttribute(title:string) =
  inherit System.Attribute()
  member self.Title = title

///Description of the study.
type DescriptionAttribute(desc:string) =
  inherit System.Attribute()
  member self.Description = desc

///Indicates that for studies with different input series, the said series need not belong
///to the same securty. For instance, if you compute the covariance, you may need
///the close of two stocks, while if you compute the daily high/low range, the input series
///shall belong to the same security.
type MultipleInputSeriesAttribute() =
  inherit System.Attribute()

//
//  Output flags
//

///Indicates that the study should be displayed on the same chart as
///the input series.
type OverlayAttribute() =
  inherit System.Attribute()

///When the study outputs several several series, this attribute
///allows to indicate their name by seperating them with a comma
///(e.g. [<OutputSeriesNames("a,b,c")>]).
type OutputSeriesNamesAttribute(series:string) =
  inherit System.Attribute()
  let series =
    series.Split([|','|], StringSplitOptions.RemoveEmptyEntries)
    |> Array.map (fun s -> s.Trim())
  member self.Series = series

//
//  Input Parameters flags
//

//To be put into practice in the various functions

///Indicate the name of the parameter.
type DisplayNameAttribute(name:string) =
  inherit System.Attribute()
  member self.Name = name

///Indicates the parameter is a bool.
type BoolAttribute() =
  inherit System.Attribute()

///Indicates that all values of the input series shall be positive.
type AllPositiveAttribute() =
  inherit System.Attribute()

///Indicates the default value of the parameter as a string.
///Obviously, you must know the parameter type to use this piece
///of information.
type DefaultValueAttribute(v:string) =
  inherit System.Attribute()
  member x.Value = v

///Indicates the minimum, maximum and step of a numeric parameter.
///Values are strings so that it can be used for any parameter type.
///Obviously, you must know the latter to use this piece
///of information.
type NumericAttribute(minV:string, maxV:string, step:string) =
  inherit System.Attribute()
  member x.MinValue = minV
  member x.MaxValue = maxV
  member x.Step = step     

module internal Study =

  let checkPositiveReal v =
    if v < 0.0 then raise <| BadParam "negative real"

  let checkPositiveInt v =
    if v < 0 then raise <| BadParam "negative int"

  let checkPositiveIntMin1 v =
    if v < 1 then raise <| BadParam "int less than one"

  let checkVolume v =
    if Array.exists (fun x -> x < 0.0) v then raise <| BadParam "negative volume"

  let checkSameInputLength xs =
    let len = Seq.head xs |> Seq.length
    if Seq.exists (fun x -> Seq.length x <> len) xs then
       raise <| BadParam "input series have different length"

  let checkLength x n =
    if Seq.length x <> n then
      raise <| BadParam "unexpected input length"

  let rec checkHighLow hs ls =
    checkSameInputLength [hs; ls]
    checkHL hs ls 0 true

  and checkHL hs ls i ok =
    if not ok then
      let msg = sprintf "low > high at %d" i
      raise <| BadParam msg
    elif i < Array.length hs then
      if hs.[i] >= ls.[i] then
        checkHL hs ls (i+1) true
      else
        checkHL hs ls i false

  let rec checkHighLowClose hs ls cs =
    checkSameInputLength [hs; ls; cs]
    checkHLC hs ls cs 0 true

  and checkHLC hs ls cs i ok =
    if not ok then
      let msg = sprintf "low > high at %d" i
      raise <| BadParam msg
    elif i < Array.length hs then
      if hs.[i] >= cs.[i] && cs.[i] >= ls.[i] then
        checkHLC hs ls cs (i+1) true
      else
        checkHLC hs ls cs i false

  let rec checkOpenHighLowClose os hs ls cs =
    checkSameInputLength [os; hs; ls; cs]
    checkOHLC os hs ls cs 0 true

  and checkOHLC os hs ls cs i ok =
    if not ok then
      let msg = sprintf "low > high at %d" i
      raise <| BadParam msg
    elif i < Array.length hs then
      if isOkOHLC os hs ls cs i then
        checkOHLC os hs ls cs (i+1) true
      else
        checkOHLC os hs ls cs i false

  and isOkOHLC os hs ls cs i =
      if cs.[i] > os.[i] then
        hs.[i] >= cs.[i] && os.[i] >= ls.[i]
      else
        hs.[i] >= os.[i] && cs.[i] >= ls.[i]

  let lazyCompute (startIdx:int) endIdx f =
    if startIdx > endIdx then [||] else f (endIdx - startIdx + 1)

  let lazyCompute2 (startIdx:int) endIdx f =
    if startIdx > endIdx then [||], [||] else f (endIdx - startIdx + 1)

  let lazyCompute3 (startIdx:int) endIdx f =
    if startIdx > endIdx then [||],[||],[||] else f (endIdx - startIdx + 1)

  let lazyCompute4 (startIdx:int) endIdx f =
    if startIdx > endIdx then [||],[||],[||],[||] else f (endIdx - startIdx + 1)

  let median (x:float[]) =
    let a = Array.sort x
    let n = Array.length a
    let middle = n / 2
    if n = 2 * middle then
      0.5 * (a.[middle] + a.[middle - 1])
    else
      a.[middle] 

  let circularIndex idx data = idx % Array.length data 

  let degToRadConversionFactor = 180.0 / System.Math.PI

  let degToRad x = x * degToRadConversionFactor

  let radToDeg x = x / degToRadConversionFactor

  let slopeToAngle isRadian slope =
    if isRadian then atan slope else (atan slope) / System.Math.PI * 180.0

  let safeWAdd (data:float[]) (data2:float[]) lookback a b =
    if data.Length = data2.Length then
      let f =
        match a, b with
        | 0.0, 0.0 -> fun x y -> 0.0
        | 0.0, 1.0 -> fun x y -> y
        | 0.0, _ -> fun x y -> b * y
        | 1.0, 0.0 -> fun x y -> x
        | _, 0.0 -> fun x y -> a * x
        | 1.0, 1.0 -> fun x y -> x + y
        | _, _ -> fun x y -> a*x + b*y
      Array.map2 f data data2
    else
      let offset = data.Length - data2.Length
      if offset > 0 then
        let f =
          match a, b with
          | 0.0, 0.0 -> fun i y -> 0.0
          | 0.0, 1.0 -> fun i y -> y
          | 0.0, _ -> fun i y -> b * y
          | 1.0, 0.0 -> fun i y -> data.[i+lookback]
          | _, 0.0 -> fun i y -> a*data.[i+lookback]
          | 1.0, 1.0 -> fun i y -> data.[i+lookback] + y
          | _, _ -> fun i y -> a*data.[i+lookback] + b*y
        Array.mapi f data2
      else
        let f =
          match a, b with
          | 0.0, 0.0 -> fun i x -> 0.0
          | 0.0, 1.0 -> fun i x -> x
          | 0.0, _ -> fun i x -> a * x
          | 1.0, 0.0 -> fun i x -> data2.[i-lookback]
          | _, 0.0 -> fun i x -> b*data2.[i-lookback]
          | 1.0, 1.0 -> fun i x -> x + data2.[i-lookback]
          | _, _ -> fun i x -> a*x + b*data2.[i-lookback]
        Array.mapi f data

  let extremas data =
    data |> Array.fold (fun (curMin, curMax) x ->
      if x < curMin then (x, curMax)
      elif x > curMax then (curMin, x)
      else (curMin, curMax)
    ) (data.[0], data.[0])

  let toRange newMin newMAx data =
    let newRange = newMAx - newMin
    let oldMin, oldMAx = extremas data
    let oldRange = oldMAx - oldMin
    if oldRange <> 0.0 then
      let convert x =
        let normalizedX = (x - oldMin) / oldRange
        newMin + (normalizedX * newRange)
      Array.map convert data
    else
      Array.create (Array.length data) (newMin + (newRange * 0.5))