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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
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
[<Fact>]
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)
[<Fact>]
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)
[<Fact>]
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)))
[<Fact>]
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)
[<Fact>]
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)
[<Fact>]
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)
[<Fact>]
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)))
[<Fact>]
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)
|