diff options
Diffstat (limited to 'src/FundLab.Domain')
| -rw-r--r-- | src/FundLab.Domain/CashLedger.fs | 118 | ||||
| -rw-r--r-- | src/FundLab.Domain/FundLab.Domain.fsproj | 1 |
2 files changed, 119 insertions, 0 deletions
diff --git a/src/FundLab.Domain/CashLedger.fs b/src/FundLab.Domain/CashLedger.fs new file mode 100644 index 0000000..d9d8b32 --- /dev/null +++ b/src/FundLab.Domain/CashLedger.fs @@ -0,0 +1,118 @@ +namespace FundLab.Domain + +open System + +/// Cash-ledger event sourcing and reconciliation for a fund. The ledger records every +/// movement of the fund's cash (available plus reserved) with a source category so the +/// statement can be grouped and checked against the actual cash balance. Amounts are +/// signed: positive is an inflow, negative an outflow. +module CashLedger = + /// Source category of a cash movement. The set is the vocabulary surfaced by the + /// reconciliation API/UI; not every category is reachable until the corresponding + /// operation actually moves cash. + type CashEventSource = + | Subscription + | Redemption + | Dividend + | Sip + | StockBuy + | StockSell + | BondBuy + | BondSell + | BondCoupon + | BondMaturity + | CapitalDeposit + + let sourceText (source: CashEventSource) : string = + match source with + | Subscription -> "subscription" + | Redemption -> "redemption" + | Dividend -> "dividend" + | Sip -> "sip" + | StockBuy -> "stock_buy" + | StockSell -> "stock_sell" + | BondBuy -> "bond_buy" + | BondSell -> "bond_sell" + | BondCoupon -> "bond_coupon" + | BondMaturity -> "bond_maturity" + | CapitalDeposit -> "capital_deposit" + + let parseSource (text: string) : CashEventSource option = + match (if isNull text then "" else text.Trim().ToLowerInvariant()) with + | "subscription" -> Some Subscription + | "redemption" -> Some Redemption + | "dividend" -> Some Dividend + | "sip" -> Some Sip + | "stock_buy" -> Some StockBuy + | "stock_sell" -> Some StockSell + | "bond_buy" -> Some BondBuy + | "bond_sell" -> Some BondSell + | "bond_coupon" -> Some BondCoupon + | "bond_maturity" -> Some BondMaturity + | "capital_deposit" -> Some CapitalDeposit + | _ -> None + + /// A scheduled/automated contribution is tagged as SIP; any other subscription key + /// is a manual subscription. The key prefix is the only origin signal carried by the + /// order pipeline, and it is set by the callers that own the schedule. + let subscriptionSourceForKey (key: string) : CashEventSource = + let text = if isNull key then "" else key + + if text.StartsWith("sip:", StringComparison.Ordinal) + || text.StartsWith("investment-plan:", StringComparison.Ordinal) then + Sip + else + Subscription + + type CashLedgerEvent = + { + Source: CashEventSource + EventDate: DateOnly + Amount: decimal + ReferenceType: string + ReferenceId: string + Note: string option + } + + type SourceTotal = + { + Source: CashEventSource + NetAmount: decimal + EventCount: int + } + + type CashReconciliation = + { + OpeningCash: decimal + NetInflow: decimal + ClosingCash: decimal + LedgerBalance: decimal + Difference: decimal + Sources: SourceTotal list + } + + /// Opening cash plus the net of every ledger event must equal the fund's actual cash + /// balance. `Difference` is zero exactly when the ledger fully explains the balance. + let reconcile (openingCash: decimal) (ledgerBalance: decimal) (events: CashLedgerEvent list) : CashReconciliation = + let netInflow = events |> List.sumBy (fun event -> event.Amount) + let closingCash = openingCash + netInflow + + let sources = + events + |> List.groupBy (fun event -> event.Source) + |> List.map (fun (source, group) -> + { + Source = source + NetAmount = group |> List.sumBy (fun event -> event.Amount) + EventCount = group.Length + }) + |> List.sortBy (fun total -> sourceText total.Source) + + { + OpeningCash = openingCash + NetInflow = netInflow + ClosingCash = closingCash + LedgerBalance = ledgerBalance + Difference = closingCash - ledgerBalance + Sources = sources + } diff --git a/src/FundLab.Domain/FundLab.Domain.fsproj b/src/FundLab.Domain/FundLab.Domain.fsproj index 6d6a8ac..7c6db3f 100644 --- a/src/FundLab.Domain/FundLab.Domain.fsproj +++ b/src/FundLab.Domain/FundLab.Domain.fsproj @@ -14,6 +14,7 @@ <Compile Include="InvestmentPlan.fs" /> <Compile Include="Rebalance.fs" /> <Compile Include="Dividend.fs" /> + <Compile Include="CashLedger.fs" /> <Compile Include="BondRules.fs" /> <Compile Include="StockRules.fs" /> <Compile Include="Performance.fs" /> |
