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