summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests/FundValuationTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 08:07:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 08:07:00 +0800
commitb05e728ae23d088ca6c9ecccf6ed00d2ab6f3839 (patch)
tree4846648aa87263742c1181c189287b53c3bf84de /tests/FundLab.Api.Tests/FundValuationTests.fs
parent27a85d9070abd000245b2a2e6460ddf9fd5eb97e (diff)
downloadfund-lab-b05e728ae23d088ca6c9ecccf6ed00d2ab6f3839.tar.gz
Add bond full milestone: profile probe, coupon/accrual rules, ledger and point-in-time valuation (3d-29)
Diffstat (limited to 'tests/FundLab.Api.Tests/FundValuationTests.fs')
-rw-r--r--tests/FundLab.Api.Tests/FundValuationTests.fs77
1 files changed, 76 insertions, 1 deletions
diff --git a/tests/FundLab.Api.Tests/FundValuationTests.fs b/tests/FundLab.Api.Tests/FundValuationTests.fs
index 1897fd3..197686d 100644
--- a/tests/FundLab.Api.Tests/FundValuationTests.fs
+++ b/tests/FundLab.Api.Tests/FundValuationTests.fs
@@ -19,6 +19,17 @@ module FundValuationSupport =
AccruedInterest = None
Date = None
MaturityDate = None
+ ParValue = None
+ IssuePrice = None
+ ValueDate = None
+ ListingDate = None
+ PublishDate = None
+ PayInterestDay = None
+ CouponRate = None
+ CouponRateExplain = None
+ BondExpireYears = None
+ Rating = None
+ DataStatus = None
}
let stockProbeReturning (prices: Map<string, decimal>) =
@@ -109,12 +120,52 @@ type FundValuationTests(fixture: PostgresFixture) =
repository().CreateBondTrade(
key,
fundId,
- { InstrumentCode = code; BondName = Some "南航转债"; Quantity = quantity; Price = price }
+ {
+ InstrumentCode = code
+ BondName = Some "南航转债"
+ Quantity = quantity
+ Price = price
+ CleanPrice = price
+ AccruedInterest = 0m
+ ParValue = 100m
+ SettlementDate = DateOnly(2026, 9, 21)
+ CouponRate = None
+ ValueDate = None
+ MaturityDate = None
+ TradeDate = Some(DateOnly(2026, 9, 21))
+ }
)
with
| BondTradeWriteResult.BondTradeCreated _ -> ()
| other -> failwithf "unexpected bond trade result: %A" other
+ let buyBondWithTerms fundId code quantity cleanPrice =
+ let key = fixture.Key(sprintf "valuation-bond-terms-%s" (Guid.NewGuid().ToString("N")))
+
+ match
+ repository().CreateBondTrade(
+ key,
+ fundId,
+ {
+ InstrumentCode = code
+ BondName = Some "南航转债"
+ Quantity = quantity
+ Price = cleanPrice
+ CleanPrice = cleanPrice
+ AccruedInterest = 0m
+ ParValue = 100m
+ SettlementDate = DateOnly(2026, 9, 21)
+ CouponRate = Some 0.02m
+ ValueDate = Some(DateOnly(2020, 10, 15))
+ MaturityDate = Some(DateOnly(2026, 10, 15))
+ TradeDate = Some(DateOnly(2026, 9, 21))
+ }
+ )
+ with
+ | BondTradeWriteResult.BondTradeCreated _ -> ()
+ | other -> failwithf "unexpected bond trade result: %A" other
+
+
let app stockPrices bondPrices =
App.createApplicationWithProbes (repository ()) (FundValuationSupport.probes stockPrices bondPrices)
@@ -143,6 +194,30 @@ type FundValuationTests(fixture: PostgresFixture) =
Assert.Contains("\"unavailablePositions\":0", body)
[<Fact>]
+ member _.``bond valuation accrues interest point-in-time and values on a dirty basis``() =
+ let fundId = createFund 10000.00m
+ buyBondWithTerms fundId "110075" 10m 106.368m
+
+ let valuationAt asOf =
+ PersistenceTestHelpers.invoke
+ (app Map.empty (Map.ofList [ "110075", 106.368m ]))
+ "GET"
+ (sprintf "/api/funds/%O/valuation?asOfDate=%s" fundId asOf)
+ [ "Authorization", "Bearer test-token" ]
+ ""
+
+ let status, early = valuationAt "2026-09-21"
+ Assert.Equal(200, status)
+ Assert.Contains("\"accruedInterest\":\"1.86849300\"", early)
+ Assert.Contains("\"dirtyPrice\":\"108.23649300\"", early)
+ Assert.Contains("\"valueBasis\":\"dirty\"", early)
+ Assert.Contains("\"marketValue\":\"1082.36\"", early)
+
+ let _, later = valuationAt "2026-10-01"
+ Assert.Contains("\"accruedInterest\":\"1.92328800\"", later)
+ Assert.Contains("\"marketValue\":\"1082.91\"", later)
+
+ [<Fact>]
member _.``all-unavailable valuation keeps cash intact and marks positions unavailable without zeroing``() =
let fundId = createFund 5000.00m
buyStock fundId "600519" 100m 1000.00m