summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--qa/driver/browser-test.js23
-rw-r--r--src/FundLab.Api/App.fs2
-rw-r--r--src/FundLab.Api/Persistence.fs140
-rw-r--r--tests/FundLab.Api.Tests/CashReconciliationTests.fs144
-rw-r--r--tests/FundLab.Api.Tests/FundValuationTests.fs9
-rw-r--r--tests/FundLab.Api.Tests/RebalanceStockTests.fs3
-rw-r--r--tests/FundLab.Api.Tests/StockSellTests.fs8
-rw-r--r--tests/FundLab.Api.Tests/StockTradeTests.fs2
8 files changed, 264 insertions, 67 deletions
diff --git a/qa/driver/browser-test.js b/qa/driver/browser-test.js
index fda7c6f..37c27a0 100644
--- a/qa/driver/browser-test.js
+++ b/qa/driver/browser-test.js
@@ -921,6 +921,29 @@ async function stockTradeScenario(page) {
const token = process.env.QA_TOKEN || "qa-token";
+ // 3d-34: 手动买入现在会扣减可用现金;P6 再平衡后该基金仅剩 0.01,
+ // 因此先追加资金,使股票/债券买入能够成交并保持 B6 现金守恒成立。
+ const depositStatus = await page.evaluate(
+ async ({ token, fundId }) => {
+ const randomKey = () => {
+ if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID().replace(/-/g, "");
+ return Math.random().toString(16).slice(2).padEnd(32, "0").slice(0, 32);
+ };
+ const response = await fetch(`/api/funds/${fundId}/capital/deposit`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "Idempotency-Key": randomKey(),
+ },
+ body: JSON.stringify({ amount: "130000.00", note: "3d-34 手动买入前追加资金" }),
+ });
+ return response.status;
+ },
+ { token, fundId: await currentFundId(page) }
+ );
+ check("S4z 手动买入前追加资金成功", depositStatus === 201, `status=${depositStatus}`);
+
lastStockTradeResponse = null;
lastStockPositionsResponse = null;
await page.fill(".stock-code-input", "600519");
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs
index c14ab66..4facf2b 100644
--- a/src/FundLab.Api/App.fs
+++ b/src/FundLab.Api/App.fs
@@ -2863,6 +2863,8 @@ module App =
return! invokeHandler (errorResponse 400 "INVALID_BOND_TRADE_REQUEST" message) next ctx
| BondTradeWriteResult.BondTradeFundNotFound ->
return! invokeHandler (errorResponse 404 "FUND_NOT_FOUND" "fund was not found") next ctx
+ | BondTradeWriteResult.BondTradeInsufficientFunds message ->
+ return! invokeHandler (errorResponse 400 "INSUFFICIENT_FUNDS" message) next ctx
with _ ->
return! invokeHandler (errorResponse 500 "PERSISTENCE_ERROR" "bond trade persistence failed") next ctx
}
diff --git a/src/FundLab.Api/Persistence.fs b/src/FundLab.Api/Persistence.fs
index 2ca11cd..f18ad68 100644
--- a/src/FundLab.Api/Persistence.fs
+++ b/src/FundLab.Api/Persistence.fs
@@ -554,6 +554,7 @@ type BondTradeWriteResult =
| BondTradeIdempotencyConflict
| BondTradeInvalid of string
| BondTradeFundNotFound
+ | BondTradeInsufficientFunds of string
type SipPlanRecord =
{
@@ -5999,7 +6000,7 @@ type FundRepository(connectionString: string) =
let costCash = Decimal.Round(normalized.Quantity * normalized.Price, 2, MidpointRounding.AwayFromZero)
let debited =
- if defaultArg debitAvailableCash false then
+ if defaultArg debitAvailableCash true then
use cashCommand =
commandWithTransaction
connection
@@ -6037,7 +6038,7 @@ type FundRepository(connectionString: string) =
insertStockTrade connection (Some transaction) trade
insertStockTradeIdempotency connection (Some transaction) idempotencyKey fingerprint trade.Id fundId
- if defaultArg debitAvailableCash false then
+ if defaultArg debitAvailableCash true then
insertCashLedgerEvent
connection
(Some transaction)
@@ -6481,7 +6482,7 @@ type FundRepository(connectionString: string) =
raise error
- member _.CreateBondTrade(idempotencyKey: string, fundId: Guid, command: BondTradeCommand, ?executedAtOverride: DateTimeOffset) : BondTradeWriteResult =
+ member _.CreateBondTrade(idempotencyKey: string, fundId: Guid, command: BondTradeCommand, ?executedAtOverride: DateTimeOffset, ?debitAvailableCash: bool) : BondTradeWriteResult =
if String.IsNullOrWhiteSpace idempotencyKey then
BondTradeWriteResult.BondTradeInvalid "idempotency key cannot be empty"
else
@@ -6544,61 +6545,96 @@ type FundRepository(connectionString: string) =
MidpointRounding.AwayFromZero
)
- let trade: BondTradeRecord =
- {
- Id = Guid.NewGuid()
- FundId = fundId
- InstrumentCode = normalized.InstrumentCode
- BondName = normalized.BondName
- Quantity = normalized.Quantity
- Price = normalized.Price
- CleanPrice = normalized.CleanPrice
- AccruedInterest = normalized.AccruedInterest
- ParValue = normalized.ParValue
- SettlementDate = normalized.SettlementDate
- CouponRate = normalized.CouponRate
- ValueDate = normalized.ValueDate
- MaturityDate = normalized.MaturityDate
- TradeDate = normalized.TradeDate |> Option.defaultValue normalized.SettlementDate
- CostCash = costCash
- IsSynthetic = isSynthetic
- ExecutedAt = executedAt
- }
+ let debited =
+ if defaultArg debitAvailableCash true then
+ use cashCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ "UPDATE funds SET available_cash = available_cash - @cost WHERE id = @fund_id AND available_cash >= @cost"
- insertBondTrade connection (Some transaction) trade
- insertBondTradeIdempotency connection (Some transaction) idempotencyKey fingerprint trade.Id fundId
+ addParameter cashCommand "cost" NpgsqlDbType.Numeric (box costCash) |> ignore
+ addParameter cashCommand "fund_id" NpgsqlDbType.Uuid (box fundId) |> ignore
+ cashCommand.ExecuteNonQuery() = 1
+ else
+ true
- use positionCommand =
- commandWithTransaction
- connection
- (Some transaction)
- """
- INSERT INTO bond_positions
- (fund_id, instrument_code, bond_name, quantity, cost_cash, last_traded_at)
- VALUES (@fund_id, @code, @name, @quantity, @cost_cash, @last_traded_at)
- ON CONFLICT (fund_id, instrument_code) DO UPDATE
- SET quantity = bond_positions.quantity + EXCLUDED.quantity,
- cost_cash = bond_positions.cost_cash + EXCLUDED.cost_cash,
- bond_name = COALESCE(EXCLUDED.bond_name, bond_positions.bond_name),
- last_traded_at = EXCLUDED.last_traded_at
- """
+ if not debited then
+ transaction.Rollback()
- addParameter positionCommand "fund_id" NpgsqlDbType.Uuid (box fundId) |> ignore
- addParameter positionCommand "code" NpgsqlDbType.Text (box normalized.InstrumentCode) |> ignore
+ BondTradeWriteResult.BondTradeInsufficientFunds(
+ sprintf
+ "available cash is not enough for a bond purchase of %s"
+ (costCash.ToString("0.00", CultureInfo.InvariantCulture))
+ )
+ else
+ let trade: BondTradeRecord =
+ {
+ Id = Guid.NewGuid()
+ FundId = fundId
+ InstrumentCode = normalized.InstrumentCode
+ BondName = normalized.BondName
+ Quantity = normalized.Quantity
+ Price = normalized.Price
+ CleanPrice = normalized.CleanPrice
+ AccruedInterest = normalized.AccruedInterest
+ ParValue = normalized.ParValue
+ SettlementDate = normalized.SettlementDate
+ CouponRate = normalized.CouponRate
+ ValueDate = normalized.ValueDate
+ MaturityDate = normalized.MaturityDate
+ TradeDate = normalized.TradeDate |> Option.defaultValue normalized.SettlementDate
+ CostCash = costCash
+ IsSynthetic = isSynthetic
+ ExecutedAt = executedAt
+ }
- let nameParameter =
- match normalized.BondName with
- | Some name -> box name
- | None -> box DBNull.Value
+ insertBondTrade connection (Some transaction) trade
+ insertBondTradeIdempotency connection (Some transaction) idempotencyKey fingerprint trade.Id fundId
- addParameter positionCommand "name" NpgsqlDbType.Text nameParameter |> ignore
- addParameter positionCommand "quantity" NpgsqlDbType.Numeric (box normalized.Quantity) |> ignore
- addParameter positionCommand "cost_cash" NpgsqlDbType.Numeric (box costCash) |> ignore
- addParameter positionCommand "last_traded_at" NpgsqlDbType.TimestampTz (box executedAt) |> ignore
- positionCommand.ExecuteNonQuery() |> ignore
+ if defaultArg debitAvailableCash true then
+ insertCashLedgerEvent
+ connection
+ (Some transaction)
+ fundId
+ CashLedger.BondBuy
+ (DateOnly.FromDateTime executedAt.UtcDateTime)
+ -costCash
+ "bond_buy"
+ (trade.Id.ToString("D"))
+ None
- transaction.Commit()
- BondTradeWriteResult.BondTradeCreated trade
+ use positionCommand =
+ commandWithTransaction
+ connection
+ (Some transaction)
+ """
+ INSERT INTO bond_positions
+ (fund_id, instrument_code, bond_name, quantity, cost_cash, last_traded_at)
+ VALUES (@fund_id, @code, @name, @quantity, @cost_cash, @last_traded_at)
+ ON CONFLICT (fund_id, instrument_code) DO UPDATE
+ SET quantity = bond_positions.quantity + EXCLUDED.quantity,
+ cost_cash = bond_positions.cost_cash + EXCLUDED.cost_cash,
+ bond_name = COALESCE(EXCLUDED.bond_name, bond_positions.bond_name),
+ last_traded_at = EXCLUDED.last_traded_at
+ """
+
+ addParameter positionCommand "fund_id" NpgsqlDbType.Uuid (box fundId) |> ignore
+ addParameter positionCommand "code" NpgsqlDbType.Text (box normalized.InstrumentCode) |> ignore
+
+ let nameParameter =
+ match normalized.BondName with
+ | Some name -> box name
+ | None -> box DBNull.Value
+
+ addParameter positionCommand "name" NpgsqlDbType.Text nameParameter |> ignore
+ addParameter positionCommand "quantity" NpgsqlDbType.Numeric (box normalized.Quantity) |> ignore
+ addParameter positionCommand "cost_cash" NpgsqlDbType.Numeric (box costCash) |> ignore
+ addParameter positionCommand "last_traded_at" NpgsqlDbType.TimestampTz (box executedAt) |> ignore
+ positionCommand.ExecuteNonQuery() |> ignore
+
+ transaction.Commit()
+ BondTradeWriteResult.BondTradeCreated trade
with error ->
try
transaction.Rollback()
diff --git a/tests/FundLab.Api.Tests/CashReconciliationTests.fs b/tests/FundLab.Api.Tests/CashReconciliationTests.fs
index d8dc7bb..6fb761e 100644
--- a/tests/FundLab.Api.Tests/CashReconciliationTests.fs
+++ b/tests/FundLab.Api.Tests/CashReconciliationTests.fs
@@ -83,6 +83,18 @@ type CashReconciliationTests(fixture: PostgresFixture) =
| CapitalDepositWriteResult.CapitalDepositReplayed _ -> ()
| other -> failwithf "unexpected deposit result: %A" other
+ let buyStock fundId code quantity price idempotencyKey =
+ match
+ repository().CreateStockTrade(
+ idempotencyKey,
+ fundId,
+ { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }
+ )
+ with
+ | StockTradeWriteResult.StockTradeCreated _
+ | StockTradeWriteResult.StockTradeReplayed _ -> ()
+ | other -> failwithf "unexpected stock trade result: %A" other
+
let buyStockDebit fundId code quantity price idempotencyKey =
match
repository().CreateStockTrade(
@@ -96,6 +108,19 @@ type CashReconciliationTests(fixture: PostgresFixture) =
| StockTradeWriteResult.StockTradeReplayed _ -> ()
| other -> failwithf "unexpected stock trade result: %A" other
+ let buyStockNoDebit fundId code quantity price idempotencyKey =
+ match
+ repository().CreateStockTrade(
+ idempotencyKey,
+ fundId,
+ { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price },
+ debitAvailableCash = false
+ )
+ with
+ | StockTradeWriteResult.StockTradeCreated _
+ | StockTradeWriteResult.StockTradeReplayed _ -> ()
+ | other -> failwithf "unexpected stock trade result: %A" other
+
let sellStock fundId code quantity price idempotencyKey =
match
repository().CreateStockSell(
@@ -108,7 +133,7 @@ type CashReconciliationTests(fixture: PostgresFixture) =
| StockSellWriteResult.StockSellReplayed _ -> ()
| other -> failwithf "unexpected stock sell result: %A" other
- let buyBond fundId code quantity price =
+ let buyBondWith fundId code quantity price debitAvailableCash =
match
repository().CreateBondTrade(
fixture.Key(sprintf "cash-recon-bond-%s" (Guid.NewGuid().ToString("N"))),
@@ -126,12 +151,19 @@ type CashReconciliationTests(fixture: PostgresFixture) =
ValueDate = None
MaturityDate = None
TradeDate = Some today
- }
+ },
+ debitAvailableCash = debitAvailableCash
)
with
| BondTradeWriteResult.BondTradeCreated _ -> ()
| other -> failwithf "unexpected bond trade result: %A" other
+ let buyBond fundId code quantity price =
+ buyBondWith fundId code quantity price true
+
+ let buyBondNoDebit fundId code quantity price =
+ buyBondWith fundId code quantity price false
+
let recordBondCoupon fundId code amount idempotencyKey =
match
repository().RecordBondCashflow(
@@ -229,14 +261,15 @@ type CashReconciliationTests(fixture: PostgresFixture) =
let status, body = reconciliation fundId
Assert.Equal(200, status)
Assert.Contains("\"openingCash\":\"10000.00\"", body)
- Assert.Contains("\"netInflow\":\"3450.00\"", body)
- Assert.Contains("\"closingCash\":\"13450.00\"", body)
- Assert.Contains("\"ledgerBalance\":\"13450.00\"", body)
+ Assert.Contains("\"netInflow\":\"2450.00\"", body)
+ Assert.Contains("\"closingCash\":\"12450.00\"", body)
+ Assert.Contains("\"ledgerBalance\":\"12450.00\"", body)
Assert.Contains("\"difference\":\"0.00\"", body)
Assert.Equal("5000.00", (sourceEntry body "capital_deposit").GetProperty("netAmount").GetString())
Assert.Equal("-1000.00", (sourceEntry body "stock_buy").GetProperty("netAmount").GetString())
Assert.Equal("400.00", (sourceEntry body "stock_sell").GetProperty("netAmount").GetString())
+ Assert.Equal("-1000.00", (sourceEntry body "bond_buy").GetProperty("netAmount").GetString())
Assert.Equal("50.00", (sourceEntry body "bond_coupon").GetProperty("netAmount").GetString())
Assert.Equal("-1000.00", (sourceEntry body "subscription").GetProperty("netAmount").GetString())
@@ -274,7 +307,7 @@ type CashReconciliationTests(fixture: PostgresFixture) =
deposit fundId 5000.00m (fixture.Key "cash-backfill-deposit")
buyStockDebit fundId "600519" 100m 10.00m (fixture.Key "cash-backfill-buy")
sellStock fundId "600519" 40m 10.00m (fixture.Key "cash-backfill-sell")
- buyBond fundId "110075" 10m 100.00m
+ buyBondNoDebit fundId "110075" 10m 100.00m
recordBondCoupon fundId "110075" 50.00m (fixture.Key "cash-backfill-coupon")
subscribe fundId (seedInstrument ()) 1000.00m (fixture.Key "cash-backfill-subscribe")
@@ -295,3 +328,102 @@ type CashReconciliationTests(fixture: PostgresFixture) =
Assert.Equal(expected, ledgerEventCount fundId)
Assert.Equal(0.00m, (repository().GetCashReconciliation fundId |> Result.defaultWith failwith).Difference)
+
+ [<Fact>]
+ member _.``manual stock buy debits available cash and records a stock_buy event``() =
+ let fundId = createFund 10000.00m
+ buyStock fundId "600519" 100m 10.00m (fixture.Key "cash-manual-stock-buy")
+
+ let status, body = reconciliation fundId
+ Assert.Equal(200, status)
+ Assert.Contains("\"openingCash\":\"10000.00\"", body)
+ Assert.Contains("\"closingCash\":\"9000.00\"", body)
+ Assert.Contains("\"ledgerBalance\":\"9000.00\"", body)
+ Assert.Contains("\"difference\":\"0.00\"", body)
+ Assert.Equal("-1000.00", (sourceEntry body "stock_buy").GetProperty("netAmount").GetString())
+ Assert.Equal(1, eventCount body "stock_buy")
+
+ [<Fact>]
+ member _.``manual bond buy debits available cash and records a bond_buy event``() =
+ let fundId = createFund 10000.00m
+ buyBond fundId "110075" 10m 100.00m
+
+ let status, body = reconciliation fundId
+ Assert.Equal(200, status)
+ Assert.Contains("\"openingCash\":\"10000.00\"", body)
+ Assert.Contains("\"closingCash\":\"9000.00\"", body)
+ Assert.Contains("\"ledgerBalance\":\"9000.00\"", body)
+ Assert.Contains("\"difference\":\"0.00\"", body)
+ Assert.Equal("-1000.00", (sourceEntry body "bond_buy").GetProperty("netAmount").GetString())
+ Assert.Equal(1, eventCount body "bond_buy")
+
+ [<Fact>]
+ member _.``non-debiting buys leave cash and ledger untouched``() =
+ let fundId = createFund 10000.00m
+ buyStockNoDebit fundId "600519" 100m 10.00m (fixture.Key "cash-nodebit-stock")
+ buyBondNoDebit fundId "110075" 10m 100.00m
+
+ let status, body = reconciliation fundId
+ Assert.Equal(200, status)
+ Assert.Contains("\"closingCash\":\"10000.00\"", body)
+ Assert.Contains("\"ledgerBalance\":\"10000.00\"", body)
+ Assert.Contains("\"difference\":\"0.00\"", body)
+ Assert.Equal(0, eventCount body "stock_buy")
+ Assert.Equal(0, eventCount body "bond_buy")
+
+ [<Fact>]
+ member _.``manual stock buy beyond available cash is rejected and changes nothing``() =
+ let fundId = createFund 1000.00m
+
+ let result =
+ repository().CreateStockTrade(
+ fixture.Key "cash-manual-stock-overspend",
+ fundId,
+ { InstrumentCode = "600519"; StockName = Some "贵州茅台"; Quantity = 100m; Price = 20.00m }
+ )
+
+ Assert.Equal(
+ StockTradeWriteResult.StockTradeInsufficientFunds "available cash is not enough for a stock purchase of 2000.00",
+ result
+ )
+
+ let status, body = reconciliation fundId
+ Assert.Equal(200, status)
+ Assert.Contains("\"closingCash\":\"1000.00\"", body)
+ Assert.Contains("\"difference\":\"0.00\"", body)
+ Assert.Equal(0, eventCount body "stock_buy")
+
+ [<Fact>]
+ member _.``manual bond buy beyond available cash is rejected and changes nothing``() =
+ let fundId = createFund 1000.00m
+
+ let result =
+ repository().CreateBondTrade(
+ fixture.Key "cash-manual-bond-overspend",
+ fundId,
+ {
+ InstrumentCode = "110075"
+ BondName = Some "南航转债"
+ Quantity = 10m
+ Price = 200.00m
+ CleanPrice = 200.00m
+ AccruedInterest = 0m
+ ParValue = 100m
+ SettlementDate = today
+ CouponRate = None
+ ValueDate = None
+ MaturityDate = None
+ TradeDate = Some today
+ }
+ )
+
+ Assert.Equal(
+ BondTradeWriteResult.BondTradeInsufficientFunds "available cash is not enough for a bond purchase of 2000.00",
+ result
+ )
+
+ let status, body = reconciliation fundId
+ Assert.Equal(200, status)
+ Assert.Contains("\"closingCash\":\"1000.00\"", body)
+ Assert.Contains("\"difference\":\"0.00\"", body)
+ Assert.Equal(0, eventCount body "bond_buy")
diff --git a/tests/FundLab.Api.Tests/FundValuationTests.fs b/tests/FundLab.Api.Tests/FundValuationTests.fs
index 409bfba..eacd5cd 100644
--- a/tests/FundLab.Api.Tests/FundValuationTests.fs
+++ b/tests/FundLab.Api.Tests/FundValuationTests.fs
@@ -107,7 +107,8 @@ type FundValuationTests(fixture: PostgresFixture) =
repository().CreateStockTrade(
key,
fundId,
- { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }
+ { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price },
+ debitAvailableCash = false
)
with
| StockTradeWriteResult.StockTradeCreated _ -> ()
@@ -133,7 +134,8 @@ type FundValuationTests(fixture: PostgresFixture) =
ValueDate = None
MaturityDate = None
TradeDate = Some(DateOnly(2026, 9, 21))
- }
+ },
+ debitAvailableCash = false
)
with
| BondTradeWriteResult.BondTradeCreated _ -> ()
@@ -159,7 +161,8 @@ type FundValuationTests(fixture: PostgresFixture) =
ValueDate = Some(DateOnly(2020, 10, 15))
MaturityDate = Some(DateOnly(2026, 10, 15))
TradeDate = Some(DateOnly(2026, 9, 21))
- }
+ },
+ debitAvailableCash = false
)
with
| BondTradeWriteResult.BondTradeCreated _ -> ()
diff --git a/tests/FundLab.Api.Tests/RebalanceStockTests.fs b/tests/FundLab.Api.Tests/RebalanceStockTests.fs
index 3aed5ae..caaa7c9 100644
--- a/tests/FundLab.Api.Tests/RebalanceStockTests.fs
+++ b/tests/FundLab.Api.Tests/RebalanceStockTests.fs
@@ -69,7 +69,8 @@ type RebalanceStockTests(fixture: PostgresFixture) =
repository().CreateStockTrade(
key,
fundId,
- { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }
+ { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price },
+ debitAvailableCash = false
)
with
| StockTradeWriteResult.StockTradeCreated _ -> ()
diff --git a/tests/FundLab.Api.Tests/StockSellTests.fs b/tests/FundLab.Api.Tests/StockSellTests.fs
index 972293a..fb17a50 100644
--- a/tests/FundLab.Api.Tests/StockSellTests.fs
+++ b/tests/FundLab.Api.Tests/StockSellTests.fs
@@ -129,7 +129,7 @@ type StockSellTests(fixture: PostgresFixture) =
let status, fundBody = fund sellQuotes fundId
Assert.Equal(200, status)
- Assert.Contains("\"availableCash\":\"1480.00\"", fundBody)
+ Assert.Contains("\"availableCash\":\"480.00\"", fundBody)
[<Fact>]
member _.``selling all units removes the position row and credits the proceeds``() =
@@ -150,7 +150,7 @@ type StockSellTests(fixture: PostgresFixture) =
Assert.Contains("\"positions\":[]", positionsBody)
let _, fundBody = fund sellQuotes fundId
- Assert.Contains("\"availableCash\":\"2200.00\"", fundBody)
+ Assert.Contains("\"availableCash\":\"1200.00\"", fundBody)
[<Fact>]
member _.``overselling is rejected with a reason and leaves cash and position unchanged``() =
@@ -171,7 +171,7 @@ type StockSellTests(fixture: PostgresFixture) =
Assert.Contains("\"quantity\":\"100.00000000\"", positionsBody)
let _, fundBody = fund quotes fundId
- Assert.Contains("\"availableCash\":\"1000.00\"", fundBody)
+ Assert.Contains("\"availableCash\":\"0.00\"", fundBody)
[<Fact>]
member _.``valuation after a partial sale prices only the remaining units``() =
@@ -215,7 +215,7 @@ type StockSellTests(fixture: PostgresFixture) =
Assert.DoesNotContain("\"quantity\":\"40.00000000\"", positionsBody)
let _, fundBody = fund quotes fundId
- Assert.Contains("\"availableCash\":\"1300.00\"", fundBody)
+ Assert.Contains("\"availableCash\":\"300.00\"", fundBody)
[<Fact>]
member _.``a persisted snapshot price wins over the live quote when selling``() =
diff --git a/tests/FundLab.Api.Tests/StockTradeTests.fs b/tests/FundLab.Api.Tests/StockTradeTests.fs
index f8a8492..0b52faf 100644
--- a/tests/FundLab.Api.Tests/StockTradeTests.fs
+++ b/tests/FundLab.Api.Tests/StockTradeTests.fs
@@ -46,7 +46,7 @@ type StockTradeTests(fixture: PostgresFixture) =
let command =
{
Name = "股票闭环测试 FOF"
- InitialCash = 100000.00m
+ InitialCash = 200000.00m
InitialUnitNav = 1.00000000m
IsSynthetic = true
}