namespace FundLab.Domain.Tests module BondRulesTests = open System open Xunit open FundLab.Domain let private annualCb = BondTerms.create 100m 0.02m 1 (DateOnly(2020, 10, 15)) (DateOnly(2026, 10, 15)) 10m 0 0m [] let ``coupon schedule runs from value date through maturity inclusive`` () = let schedule = BondRules.couponSchedule annualCb Assert.Equal(7, List.length schedule) Assert.Equal(DateOnly(2020, 10, 15), List.head schedule) Assert.Equal(DateOnly(2026, 10, 15), List.last schedule) Assert.Equal(DateOnly(2025, 10, 15), List.item 5 schedule) [] let ``accrued interest uses actual over actual days in the current period`` () = // Period 2025-10-15 .. 2026-10-15 is 365 days; 2026-04-15 is 182 days in. let accrued = BondRules.accruedInterest annualCb (DateOnly(2026, 4, 15)) Assert.Equal(0.997260m, accrued) [] let ``accrued interest is zero before the value date and after maturity`` () = Assert.Equal(0m, BondRules.accruedInterest annualCb (DateOnly(2020, 10, 14))) Assert.Equal(0m, BondRules.accruedInterest annualCb (DateOnly(2027, 1, 1))) [] let ``dirty price is clean price plus accrued interest`` () = Assert.Equal(106.368m, BondRules.dirtyPrice 106.368m 0m) Assert.Equal(107.368m, BondRules.dirtyPrice 106.368m 1.0m) [] let ``trade cost is quantity times price for a 100 par bond`` () = // 10 张 * 106.368 = 1063.68, matching the existing bond-trade contract. Assert.Equal(1063.68m, BondRules.tradeCost annualCb 10m 106.368m) [] let ``trade cost applies the instrument fee rate`` () = let terms = { annualCb with FeeRate = 0.0002m } // 1063.68 gross + 0.212736 -> 0.21 fee. Assert.Equal(1063.89m, BondRules.tradeCost terms 10m 106.368m) [] let ``settlement date is T+0 for exchange bonds and skips weekends otherwise`` () = Assert.Equal(DateOnly(2026, 9, 18), BondRules.settlementDate annualCb (DateOnly(2026, 9, 18))) let t1 = { annualCb with SettlementDays = 1 } Assert.Equal(DateOnly(2026, 9, 21), BondRules.settlementDate t1 (DateOnly(2026, 9, 18))) [] let ``quantity must be positive and a whole multiple of the minimum unit`` () = match BondRules.validateQuantity annualCb 10m with | Ok value -> Assert.Equal(10m, value) | Error message -> failwith message Assert.True(BondRules.validateQuantity annualCb 15m |> Result.isError) Assert.True(BondRules.validateQuantity annualCb 0m |> Result.isError) Assert.True(BondRules.validateQuantity annualCb -10m |> Result.isError)