summaryrefslogtreecommitdiff
path: root/src/FundLab.Domain/Performance.fs
blob: b1c51db64adfa7d09738d8e3f42dfd333fa81bba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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))