summaryrefslogtreecommitdiff
path: root/src/FundLab.Domain/Performance.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-20 22:59:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-20 22:59:00 +0800
commitc60905e9e7f992a7f8c79c3812e92a44b2d606f5 (patch)
treea895bc6f0ebe669407ffe4d1a11439e03a6fe0d4 /src/FundLab.Domain/Performance.fs
downloadfund-lab-c60905e9e7f992a7f8c79c3812e92a44b2d606f5.tar.gz
feat(core): 建立 fund-lab 可运行基线
[变更性质] - 本提交冻结当前可构建、可测试的应用基线,不包含 PostgreSQL 持久化。 [新增功能] - 建立 F# Domain、API、Worker、Web 及测试项目。 - 增加 Bearer 认证、健康检查、账本领域模型和中文空状态页面。 [实现方案] - 使用环境变量模板注入认证配置,并排除数据、凭证和构建产物。 - 保留 19 个 Domain 测试和 5 个 API 测试作为后续变更基准。 [影响范围] - 为后续 3a PostgreSQL FOF 创建/读取切片提供可回滚基线。 - 当前仍不接入真实基金数据、真实交易或数据库。
Diffstat (limited to 'src/FundLab.Domain/Performance.fs')
-rw-r--r--src/FundLab.Domain/Performance.fs51
1 files changed, 51 insertions, 0 deletions
diff --git a/src/FundLab.Domain/Performance.fs b/src/FundLab.Domain/Performance.fs
new file mode 100644
index 0000000..b1c51db
--- /dev/null
+++ b/src/FundLab.Domain/Performance.fs
@@ -0,0 +1,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))