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}"); } } } }