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

Comments are closed.