summaryrefslogtreecommitdiff
path: root/tests/FundLab.Domain.Tests/BondRulesTests.fs
blob: 31069f1c829aeec252e81f9eaa3abefdd621c223 (plain)
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)