summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/FundLab.Api.Tests/FundValuationTests.fs2
-rw-r--r--tests/FundLab.Api.Tests/StockQuoteProbeTests.fs18
-rw-r--r--tests/FundLab.Api.Tests/StockSellTests.fs2
-rw-r--r--tests/FundLab.Api.Tests/StockTradeTests.fs3
-rw-r--r--tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj1
-rw-r--r--tests/FundLab.Domain.Tests/StockRulesTests.fs62
6 files changed, 86 insertions, 2 deletions
diff --git a/tests/FundLab.Api.Tests/FundValuationTests.fs b/tests/FundLab.Api.Tests/FundValuationTests.fs
index 197686d..409bfba 100644
--- a/tests/FundLab.Api.Tests/FundValuationTests.fs
+++ b/tests/FundLab.Api.Tests/FundValuationTests.fs
@@ -7,7 +7,7 @@ open FundLab.Api
module FundValuationSupport =
let stockQuote (code: string) (price: decimal) : StockQuote =
- { Code = code; Name = Some "贵州茅台"; Price = Some price; Currency = "CNY" }
+ { Code = code; Name = Some "贵州茅台"; Price = Some price; Currency = "CNY"; Market = Some "sh"; Adjust = Some "none"; Suspended = Some false }
let bondQuote (code: string) (price: decimal) : BondQuote =
{
diff --git a/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs b/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs
index 2cad557..b869f17 100644
--- a/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs
+++ b/tests/FundLab.Api.Tests/StockQuoteProbeTests.fs
@@ -58,6 +58,21 @@ type StockQuoteProbeTests(fixture: PostgresFixture) =
| Error failure -> failwithf "expected a stock quote, got %A" failure
[<Fact>]
+ member _.``probe parses market, adjust and suspension flags``() =
+ let json =
+ "{\"schema_version\":\"fund-lab.akshare.v1\",\"operation\":\"stock-quote\",\"source\":\"akshare\",\"source_revision\":\"akshare-test/eastmoney\",\"collected_at\":\"2026-09-21T08:00:00Z\",\"instrument\":{\"code\":\"000001\"},\"quote\":{\"name\":\"平安银行\",\"price\":\"11.2300\",\"currency\":\"CNY\",\"market\":\"sz\",\"adjust\":\"none\",\"suspended\":true}}"
+
+ let probe =
+ AkshareStockQuoteProbe(StockQuoteProbeSupport.collectorReturning (fun () -> Ok json)) :> IStockQuoteProbe
+
+ match probe.GetQuote("000001", CancellationToken.None) with
+ | Ok quote ->
+ Assert.Equal<string option>(Some "sz", quote.Market)
+ Assert.Equal<string option>(Some "none", quote.Adjust)
+ Assert.Equal<bool option>(Some true, quote.Suspended)
+ | Error failure -> failwithf "expected a stock quote, got %A" failure
+
+ [<Fact>]
member _.``probe keeps a missing name missing instead of fabricating data``() =
let json = StockQuoteProbeSupport.payload "600519" None (Some "1252.57")
@@ -139,6 +154,9 @@ type StockQuoteProbeTests(fixture: PostgresFixture) =
Name = Some "贵州茅台"
Price = Some 1252.57m
Currency = "CNY"
+ Market = Some "sh"
+ Adjust = Some "none"
+ Suspended = Some false
})
let status, body =
diff --git a/tests/FundLab.Api.Tests/StockSellTests.fs b/tests/FundLab.Api.Tests/StockSellTests.fs
index cbdef8b..972293a 100644
--- a/tests/FundLab.Api.Tests/StockSellTests.fs
+++ b/tests/FundLab.Api.Tests/StockSellTests.fs
@@ -6,7 +6,7 @@ open FundLab.Api
module StockSellSupport =
let quote (code: string) (price: decimal) : StockQuote =
- { Code = code; Name = Some "招商银行"; Price = Some price; Currency = "CNY" }
+ { Code = code; Name = Some "招商银行"; Price = Some price; Currency = "CNY"; Market = Some "sh"; Adjust = Some "none"; Suspended = Some false }
let stubQuoteProbe (quotes: Map<string, decimal>) =
{ new IStockQuoteProbe with
diff --git a/tests/FundLab.Api.Tests/StockTradeTests.fs b/tests/FundLab.Api.Tests/StockTradeTests.fs
index a2269a1..f8a8492 100644
--- a/tests/FundLab.Api.Tests/StockTradeTests.fs
+++ b/tests/FundLab.Api.Tests/StockTradeTests.fs
@@ -20,6 +20,9 @@ module StockTradeSupport =
Name = Some "贵州茅台"
Price = Some 1252.57m
Currency = "CNY"
+ Market = Some "sh"
+ Adjust = Some "none"
+ Suspended = Some false
}
let appWithQuote (probe: IStockQuoteProbe) =
diff --git a/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj b/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj
index 468ee16..c5b87a1 100644
--- a/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj
+++ b/tests/FundLab.Domain.Tests/FundLab.Domain.Tests.fsproj
@@ -22,6 +22,7 @@
<Compile Include="DomainTests.fs" />
<Compile Include="InvestmentPlanTests.fs" />
<Compile Include="BondRulesTests.fs" />
+ <Compile Include="StockRulesTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
diff --git a/tests/FundLab.Domain.Tests/StockRulesTests.fs b/tests/FundLab.Domain.Tests/StockRulesTests.fs
new file mode 100644
index 0000000..ddf943f
--- /dev/null
+++ b/tests/FundLab.Domain.Tests/StockRulesTests.fs
@@ -0,0 +1,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)))