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

Comments are closed.