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)))
|