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]))
    )

Comments are closed.