summaryrefslogtreecommitdiff
path: root/tests/FundLab.Domain.Tests/StockRulesTests.fs
blob: ddf943f8e79f967a325f25726c37cbc1ed0ac651 (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
namespace FundLab.Domain.Tests

module StockRulesTests =

    open System
    open Xunit
    open FundLab.Domain

    let private terms = StockTerms.aShareDefault

    [<Fact>]
    let ``buy cost applies commission minimum and transfer fee`` () =
        // gross 1000.00 -> commission 0.25 floors to 5.00, transfer 0.01.
        Assert.Equal(1005.01m, StockRules.buyCost terms 100m 10m)
        // gross 100000.00 -> commission 25.00 (above the minimum), transfer 1.00.
        Assert.Equal(100026.00m, StockRules.buyCost terms 1000m 100m)

    [<Fact>]
    let ``sell proceeds deduct commission, stamp duty and transfer fee`` () =
        Assert.Equal(994.49m, StockRules.sellProceeds terms 100m 10m)
        Assert.Equal(99924.00m, StockRules.sellProceeds terms 1000m 100m)

    [<Fact>]
    let ``sell proceeds are lower than buy cost on a round trip`` () =
        Assert.True(StockRules.sellProceeds terms 100m 10m < StockRules.buyCost terms 100m 10m)

    [<Fact>]
    let ``quantity must be a whole multiple of the lot size`` () =
        match StockRules.validateQuantity terms 100m with
        | Ok value -> Assert.Equal(100m, value)
        | Error message -> failwithf "expected a valid lot, got %s" message

        match StockRules.validateQuantity terms 50m with
        | Ok _ -> failwith "expected a lot-size error"
        | Error message -> Assert.Contains("multiple of 100", message)

        match StockRules.validateQuantity terms 0m with
        | Ok _ -> failwith "expected a positive-quantity error"
        | Error message -> Assert.Contains("positive", message)

    [<Fact>]
    let ``settlement applies the cutoff then T+1 and skips the weekend`` () =
        let thursday = DateOnly(2026, 9, 24)

        // Before the 15:00 cutoff: T+1 lands on Friday.
        Assert.Equal(DateOnly(2026, 9, 25), StockRules.settlementDate terms thursday (TimeOnly(10, 0)))

        // At/after the cutoff: the order slips to Friday, T+1 lands on Monday.
        Assert.Equal(DateOnly(2026, 9, 28), StockRules.settlementDate terms thursday (TimeOnly(15, 0)))
        Assert.Equal(DateOnly(2026, 9, 28), StockRules.settlementDate terms thursday (TimeOnly(15, 30)))

    [<Fact>]
    let ``effective trade date shifts to the next trading day after the cutoff`` () =
        let thursday = DateOnly(2026, 9, 24)
        Assert.Equal(thursday, StockRules.effectiveTradeDate terms thursday (TimeOnly(14, 59)))
        Assert.Equal(DateOnly(2026, 9, 25), StockRules.effectiveTradeDate terms thursday (TimeOnly(15, 0)))

    [<Fact>]
    let ``a stock bought today cannot be sold until T+1`` () =
        let thursday = DateOnly(2026, 9, 24)
        Assert.False(StockRules.isSellAllowed terms thursday thursday)
        Assert.True(StockRules.isSellAllowed terms thursday (DateOnly(2026, 9, 25)))