using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// GLMS-20260701-0006 多次部分平仓后提前终止 Tab 序号2 平仓比例显示 32.50% 而非 50% 的回归测试 /// ================================================================================ /// 根因:SwapDealService.ApplySwapTrade 缺少 A→B 口径转换。 /// - SwapUnwind 在入口处将 ClosePercent 从口径A(占期初) 转为 口径B(占剩余),SaveSwapDealInternal 落库时 B→A 还原。 /// - ApplySwapTrade 没有做 A→B 转换,导致 SaveSwapDealInternal 的 B→A 还原出错: /// 0.50(A) → ToOriginalClosePercent(0.50, 50000000, 32500000) = 0.50*32500000/50000000 = 0.325 ❌ /// - 修复后:0.50(A) → ToRemainingClosePercent → 0.769(B) → ToOriginalClosePercent → 0.50(A) ✅ /// /// 测试策略: /// 1) 纯函数测试:验证 A→B→A 往返转换的正确性 /// 2) ApplySwapTrade 集成测试:验证 SaveSwapDeal 收到的 ClosePercent 已转为口径B /// [TestClass] public class ApplySwapTradeClosePercentBugTest { // GLMS-20260701-0006 真实数据 private const decimal OriginalNotional = 50_000_000m; // 期初名义本金 private const decimal RemainingAfter1st = 32_500_000m; // 首次平35%后剩余 private const decimal FirstClosePercent = 0.35m; // 第一次平仓比例(口径A) private const decimal SecondClosePercent = 0.50m; // 第二次平仓比例(口径A, 用户输入50%) // ================================================================ // 1) 纯函数:A→B→A 往返转换应还原原值 // ================================================================ [TestMethod] public void AC_001_口径转换_往返A到B到A应还原原值() { // 第二次部分平仓: 用户输入 50%(口径A) decimal closePercentA = SecondClosePercent; // A → B(ApplySwapTrade/SwapUnwind 入口转换) decimal closePercentB = SwapDealService.ToRemainingClosePercent( closePercentA, OriginalNotional, RemainingAfter1st); // B → A(SaveSwapDealInternal 落库还原) decimal closePercentA_restored = SwapDealService.ToOriginalClosePercent( closePercentB, OriginalNotional, RemainingAfter1st); SwapDealTestFactory.AssertDecimalEqual(closePercentA, closePercentA_restored, 1e-10m, "A→B→A 往返转换应还原原值"); Console.WriteLine($"A={closePercentA}, B={closePercentB}, A_restored={closePercentA_restored}"); } [TestMethod] public void AC_002_口径转换_未修复时B到A会得到错误的0_325() { // 模拟 bug:ApplySwapTrade 未做 A→B 转换,直接把 A 传给 SaveSwapDealInternal 的 B→A 还原 decimal closePercentA = SecondClosePercent; // 0.50 // bug 路径:SaveSwapDealInternal 误把 A 当 B 做还原 decimal buggyResult = SwapDealService.ToOriginalClosePercent( closePercentA, OriginalNotional, RemainingAfter1st); // 0.50 * 32500000 / 50000000 = 0.325 SwapDealTestFactory.AssertDecimalEqual(0.325m, buggyResult, 1e-10m, "bug 路径:0.50(A) 被误当 B 做还原 → 0.325"); Assert.AreNotEqual(SecondClosePercent, buggyResult, "bug 结果 0.325 不等于用户输入 0.50"); Console.WriteLine($"Bug: 0.50(A) 误当 B → ToOriginalClosePercent → {buggyResult} (应为 0.50)"); } // ================================================================ // 2) ApplySwapTrade 集成测试:验证 SaveSwapDeal 收到的是口径B // ================================================================ [TestMethod] public void AC_003_ApplySwapTrade_第二次部分平仓50perc_应将ClosePercent转为口径B() { // 模拟 GLMS-20260701-0006 第二次部分平仓的场景 var td = new trade { id = 1991, TradeNumber = "GLMS-20260701-0006", TradeType = "收益互换", TradeStatus = "确认成交", ValidState = "Valid", StockEqvNotional = (double)RemainingAfter1st, // 32500000 OriginalStockEqvNotional = (double)OriginalNotional, // 50000000 Notional = (double)RemainingAfter1st, TradeAmount = (double)RemainingAfter1st }; var service = new TestableSwapDealService(td); // 前端传入的 UnwindData(ClosePercent = 0.50, 口径A) var unwindData = new UnwindData { SwapTradeId = td.id, SwapRealizedPnL = 1000m, SwapCloseAmount = 1000m, CloseMethod = (int)CloseMethodEnum.部分平仓, ClosePercent = SecondClosePercent, // 0.50 (口径A, 用户输入50%) CloseQty = 25000000m, CloseNotionalValue = 25000000m, // 50% of original PositionQty = RemainingAfter1st, // 32500000 NotionalValue = OriginalNotional, // 50000000 (期初) PosiNotionalValue = RemainingAfter1st, // 32500000 (剩余) ValueDate = new DateTime(2026, 7, 14), UnwindDate = new DateTime(2026, 7, 15), StartDate = new DateTime(2026, 7, 1) }; service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓); // 验证 SaveSwapDeal 被调用 Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "ApplySwapTrade 应调用 SaveSwapDeal"); // 验证传给 SaveSwapDeal 的 ClosePercent 已转为口径B var savedData = service.SaveSwapDealCalls[0].data; decimal expectedB = SwapDealService.ToRemainingClosePercent( SecondClosePercent, OriginalNotional, RemainingAfter1st); SwapDealTestFactory.AssertDecimalEqual(expectedB, savedData.ClosePercent, 1e-10m, "ApplySwapTrade 应将 ClosePercent 从口径A转为口径B"); // 关键验证:B 值不应等于 A 值(0.50),也不应等于 bug 值(0.325) Assert.AreNotEqual(SecondClosePercent, savedData.ClosePercent, "口径B 不应等于口径A (0.50)"); Assert.AreNotEqual(0.325m, savedData.ClosePercent, "口径B 不应等于 bug 值 (0.325)"); Console.WriteLine($"输入: ClosePercent(A)={SecondClosePercent}"); Console.WriteLine($"输出: ClosePercent(B)={savedData.ClosePercent}"); Console.WriteLine($"期望: ClosePercent(B)={expectedB}"); Console.WriteLine($"往返还原: ClosePercent(A)={SwapDealService.ToOriginalClosePercent(savedData.ClosePercent, OriginalNotional, RemainingAfter1st)}"); } [TestMethod] public void AC_004_ApplySwapTrade_第一次平仓35perc_口径转换正确() { // 第一次平仓:remaining = original, 所以 A = B = 0.35 var td = new trade { id = 1991, TradeNumber = "GLMS-20260701-0006", TradeType = "收益互换", TradeStatus = "确认成交", ValidState = "Valid", StockEqvNotional = (double)OriginalNotional, OriginalStockEqvNotional = (double)OriginalNotional, Notional = (double)OriginalNotional, TradeAmount = (double)OriginalNotional }; var service = new TestableSwapDealService(td); var unwindData = new UnwindData { SwapTradeId = td.id, SwapRealizedPnL = 1000m, SwapCloseAmount = 1000m, CloseMethod = (int)CloseMethodEnum.部分平仓, ClosePercent = FirstClosePercent, // 0.35 (口径A) CloseQty = 17500000m, CloseNotionalValue = 17500000m, PositionQty = OriginalNotional, NotionalValue = OriginalNotional, PosiNotionalValue = OriginalNotional, // 首次平仓 remaining == original ValueDate = new DateTime(2026, 7, 6), UnwindDate = new DateTime(2026, 7, 7), StartDate = new DateTime(2026, 7, 1) }; service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓); var savedData = service.SaveSwapDealCalls[0].data; // 首次平仓 remaining == original → A == B == 0.35 SwapDealTestFactory.AssertDecimalEqual(FirstClosePercent, savedData.ClosePercent, 1e-10m, "首次平仓 remaining==original → 口径A==口径B==0.35"); Console.WriteLine($"首次平仓: ClosePercent(A=B)={savedData.ClosePercent}"); } } }