From fee2203d6e8d5b55c7be50a93070cca3509159c8 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 2 Jul 2026 08:49:50 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20DealFloatPositions=E6=B5=AE?= =?UTF-8?q?=E5=8A=A8=E8=85=BF=E5=BD=92=E6=A1=A3=E5=90=88=E6=88=90=E6=B5=8B?= =?UTF-8?q?=E8=AF=95(testable=E8=BF=81=E7=A7=BB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增4个浮动腿归档场景测试: - DF_001: 空持仓返回空列表 - DF_002: 首日无eod走SaveCurrentEodInitalPosi分支 - DF_003: SaveCurrentEodInitalPosi纯计算字段验证(数量/均价/类型) - DF_004: 有eod无平仓走CopyEodPosition分支(分支选择验证) DealFloatPositions和子方法无直接DB调用(数据从参数传入), 但CopyEodPosition/UpdateEodPosition依赖外部数据源 (DataCacheProvider/UnderlyingCodePrice/BondPaymentService), 值验证需后续加接缝。当前覆盖分支选择+首日初始化值验证。 验证: 104+4=108全通过。 --- .../DealFloatPositionsScenarioTest.cs | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs diff --git a/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs new file mode 100644 index 00000000..1b6f6b7b --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/DealFloatPositionsScenarioTest.cs @@ -0,0 +1,225 @@ +using YLErp.DBModels; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// DealFloatPositions 浮动腿收盘归档 - 合成单元测试 + /// ============================================================================ + /// DealFloatPositions 处理浮动腿(标的持仓)的日终归档,三个分支: + /// ① 无前日eod → SaveCurrentEodInitalPosi(首日初始化,纯计算) + /// ② 有eod无平仓 → CopyEodPosition(复制+更新价格,依赖外部数据源) + /// ③ 有eod有平仓 → UpdateEodPosition(更新持仓,依赖外部数据源) + /// + /// 当前可测范围: + /// - 分支选择逻辑(DealFloatPositions 调度层,纯内存) + /// - SaveCurrentEodInitalPosi(首日初始化,纯计算,无外部依赖) + /// CopyEodPosition/UpdateEodPosition 需额外接缝(UnderlyingCodePrice等),留后续。 + /// ============================================================================ + [TestClass] + public class DealFloatPositionsScenarioTest + { + private const int SwapTradeId = 200; + private static readonly DateTime TradeDate = new(2026, 4, 28); + private static readonly DateTime PreSettleDate = new(2026, 4, 27); + + #region Stub + + private sealed class StubEodService : SwapEodPositionService + { + public StubEodService() : base(new OptUserInfo(0, nameof(DealFloatPositionsScenarioTest), OptUserFrom.UnitTest)) + { + } + + // DealFloatPositions 和子方法都是 protected,通过 public 包装暴露 + public List ExecuteDealFloatPositions( + List posiList, List realPosiList, + List eodPositions, List todyEodPositions, + DateTime settleDate, trade td, DateTime preSettleDate, List flowEvents) + { + return DealFloatPositions(posiList, realPosiList, eodPositions, todyEodPositions, + settleDate, td, preSettleDate, flowEvents); + } + + public eod_swap_position ExecuteSaveCurrentEodInitalPosi( + swap_position position, trade td, DateTime settleDate, DateTime preSettleDate, + List unwindEvents) + { + return SaveCurrentEodInitalPosi(position, td, settleDate, preSettleDate, unwindEvents); + } + } + + #endregion + + #region 数据构建 + + private static trade CreateTrade() + { + return new trade + { + id = SwapTradeId, TradeNumber = "UT-FLOAT-001", ClientId = 999998, + TradeType = "收益互换", TradeDate = PreSettleDate, StartDate = PreSettleDate, + ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", + ValidState = "Valid", StructureType = "单标的", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", + OriginalStockEqvNotional = 10000 + }; + } + + private static swap_position CreateFloatPosition(int id = 3001, decimal qty = 10000m) + { + return new swap_position + { + id = id, SwapTradeId = SwapTradeId, + PosiDirection = 2, PositionType = (int)PositionTypeFlag.Long, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PosiQuantity = qty, PosiNotionalValue = qty, + PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m, + PosiNetFeePrice = 1.004m, PosiNetNoFeePrice = 1.001m, + IsInitial = true, Invalid = false, + PosiTradingFee = 0, PosiTradingFeePending = 0 + }; + } + + private static swap_flow_event CreateCloseEvent(int positionId, decimal qty, decimal markClosePnl = 100m) + { + return new swap_flow_event + { + SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.平仓, + PositionId = positionId, Quantity = qty, + MarkClosePnl = markClosePnl, DividendIn = 0, CloseFee = 5m, + TradingFeePending = 0, TradingAmount = qty * 1.002m, + UnwindDate = TradeDate, EventDate = TradeDate, PayDate = TradeDate, + DataState = (int)SwapFlowDateStateEnum.完成 + }; + } + + #endregion + + // ================================================================ + // 场景1:空持仓列表 → 返回空列表 + // ================================================================ + + [TestMethod] + public void DF_001_空持仓返回空列表() + { + var service = new StubEodService(); + var result = service.ExecuteDealFloatPositions( + new List(), new List(), + new List(), new List(), + TradeDate, CreateTrade(), PreSettleDate, new List()); + + Assert.AreEqual(0, result.Count, "空持仓应返回空列表"); + } + + // ================================================================ + // 场景2:首日无前日eod → 走 SaveCurrentEodInitalPosi 分支 + // ================================================================ + + /// + /// 无前日eod(eodPositions 不含该持仓),应走 SaveCurrentEodInitalPosi。 + /// SaveCurrentEodInitalPosi 是纯计算,验证基本字段正确。 + /// + [TestMethod] + public void DF_002_首日无eod走初始化分支() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var result = service.ExecuteDealFloatPositions( + new List { position }, + new List { position }, + new List(), // 无前日eod + new List(), // 无当日eod + TradeDate, td, PreSettleDate, + new List()); // 无平仓事件 + + Assert.AreEqual(1, result.Count, "应生成1条浮动腿eod"); + var eod = result[0]; + Assert.AreEqual(position.id, eod.PositionId, "PositionId应匹配"); + Assert.AreEqual(SwapTradeId, eod.SwapTradeId); + Assert.AreEqual(TradeDate, eod.ValueDate); + Console.WriteLine($"首日初始化: PosiQuantity={eod.PosiQuantity}, PosiNetPrice={eod.PosiNetPrice}"); + } + + // ================================================================ + // 场景3:SaveCurrentEodInitalPosi 直接验证(纯计算方法) + // ================================================================ + + /// + /// 直接测 SaveCurrentEodInitalPosi,验证它正确初始化 eod 的关键字段。 + /// 这个方法无外部依赖(纯计算),可以精确验证值。 + /// + [TestMethod] + public void DF_003_首日初始化字段正确() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(qty: 10000m); + + var eod = service.ExecuteSaveCurrentEodInitalPosi( + position, td, TradeDate, PreSettleDate, new List()); + + // 验证关键字段 + Assert.AreEqual(10000m, eod.PosiQuantity, "持仓数量应=初始数量"); + Assert.AreEqual(1.005m, eod.PosiNetPrice, "含费均价应=持仓均价"); + Assert.AreEqual(1.002m, eod.PosiGrossPrice, "不含费均价"); + Assert.AreEqual((int)PositionTypeFlag.Long, eod.PositionType, "持仓类型"); + Assert.AreEqual(SwapTradeId, eod.SwapTradeId, "交易ID"); + Assert.AreEqual(td.ClientId, eod.ClientId, "客户ID"); + Assert.AreEqual(0, eod.TdCloseQty, "首日无平仓数量"); + Assert.AreEqual(0, eod.TdCloseMtmPnl, "首日无平仓盈亏"); + Console.WriteLine($"首日初始化 eod: Qty={eod.PosiQuantity}, NetPrice={eod.PosiNetPrice}, GrossPrice={eod.PosiGrossPrice} ✅"); + } + + // ================================================================ + // 场景4:有前日eod无平仓 → 走 CopyEodPosition 分支 + // ================================================================ + + /// + /// 有前日eod但无平仓事件,应走 CopyEodPosition 分支。 + /// CopyEodPosition 依赖外部数据源(DataCacheProvider/UnderlyingCodePrice), + /// 测试验证分支选择正确(不验证值),且不抛异常。 + /// + [TestMethod] + public void DF_004_有eod无平仓走Copy分支() + { + var service = new StubEodService(); + var td = CreateTrade(); + var position = CreateFloatPosition(); + + var preEod = new eod_swap_position + { + id = 5001, SwapTradeId = SwapTradeId, PositionId = position.id, + ValueDate = PreSettleDate, PosiQuantity = 10000m, + PosiNetPrice = 1.005m, PosiGrossPrice = 1.002m, + UnderlyingCode = "210210.IB", ContractSize = 1m, + PositionType = (int)PositionTypeFlag.Long, PosiDirection = 2 + }; + + // CopyEodPosition 内部调 DataCacheProvider/UnderlyingCodePrice, + // 这些连缓存可能返回null → 方法 cs:1492 if(um==null) return curretEod + // 所以即使缓存没数据,也不会抛异常,只是字段不更新 + try + { + var result = service.ExecuteDealFloatPositions( + new List { position }, + new List { position }, + new List { preEod }, + new List(), + TradeDate, td, PreSettleDate, + new List()); // 无平仓 + + Assert.AreEqual(1, result.Count, "应生成1条eod"); + // um==null时 CopyEodPosition 直接返回 clone,字段不变 + Assert.AreEqual(10000m, result[0].PosiQuantity, "无缓存时数量应=前日值"); + Console.WriteLine($"Copy分支(无缓存): PosiQuantity={result[0].PosiQuantity}(保持前日值)"); + } + catch (Exception ex) + { + Assert.Inconclusive($"CopyEodPosition 依赖外部数据源,需额外接缝。异常: {ex.Message}"); + } + } + } +}