namespace FundLab.Domain open System type PerformanceObservation = { At: DateTimeOffset NetAssets: decimal ExternalCashFlow: decimal } type PerformanceError = | InvalidObservation of string module Performance = let timeWeightedReturn observations = let validate observation previousAt = if observation.NetAssets < 0m then Error(InvalidObservation "net assets cannot be negative") elif previousAt |> Option.exists (fun at -> observation.At <= at) then Error(InvalidObservation "observations must be strictly ordered") else Ok() match observations with | [] -> Ok 0m | first :: rest -> validate first None |> Result.bind (fun () -> if first.NetAssets <= 0m then Error(InvalidObservation "first net assets must be positive") else rest |> List.fold (fun result observation -> result |> Result.bind (fun (previous, linkedReturn) -> validate observation (Some previous.At) |> Result.bind (fun () -> let endingAssetsBeforeFlow = observation.NetAssets - observation.ExternalCashFlow if endingAssetsBeforeFlow < 0m then Error(InvalidObservation "external cash flow exceeds ending net assets") elif previous.NetAssets <= 0m then Error(InvalidObservation "period start net assets must be positive") else let periodReturn = endingAssetsBeforeFlow / previous.NetAssets Ok(observation, linkedReturn * periodReturn))) ) (Ok(first, 1m)) |> Result.map (fun (_, linkedReturn) -> linkedReturn - 1m))