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