From 43679393bb32e57830c4db728bcd4c44b47436c9 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 17 Jul 2026 08:23:50 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20=E8=A1=A5=20e2fb456b=20InitUnwind?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A4=20ClosePercent=20=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽 CalcDefaultInitClosePercent 静态纯函数(PosiNotionalValue/NotionalValue), InitUnwind L270 改为调用此函数, 新增 6 个单元测试覆盖: 未平仓/已平30%/除零保护/GLMS-20260701-0013 真实快照/与 ToRemainingClosePercent 联动自洽不变式/全平完边界场景. 填补 e2fb456b 提交的测试覆盖缺口. --- .../InitUnwindDefaultClosePercentTest.cs | 103 ++++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 16 ++- 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/InitUnwindDefaultClosePercentTest.cs diff --git a/UnitTestProject/Modules/SwapModule/InitUnwindDefaultClosePercentTest.cs b/UnitTestProject/Modules/SwapModule/InitUnwindDefaultClosePercentTest.cs new file mode 100644 index 00000000..dfc268b9 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/InitUnwindDefaultClosePercentTest.cs @@ -0,0 +1,103 @@ +namespace YLErp.Modules.SwapModule +{ + /// + /// InitUnwind 默认 ClosePercent 计算的回归测试。 + /// --------------------------------------------------------------- + /// 守卫提交 e2fb456b "fix 平仓(InitUnwind L267)硬编码 1 而不是剩余平仓比例"。 + /// + /// 旧 bug:InitUnwind 默认把 ClosePercent 硬编码为 1(按"占剩余 100%"), + /// 但前端约定 ClosePercent 是"占期初(original)"口径(A),1 表示平掉原始本金的 100%。 + /// 多次部分平仓后剩余本金 < 期初本金,此时默认 1 在前端语义上意味着"还要平掉原始全部", + /// 与"平掉剩余全部"意图不符,且会触发后端 ToRemainingClosePercent 转换后 >1 被 cap 到 1, + /// 表面看无差异但语义混乱,且若前端 / 事件展示直接用此值会出错。 + /// + /// 修复:ClosePercent = PosiNotionalValue / NotionalValue(占期初口径的"平剩余全部")。 + /// 抽出为纯函数 CalcDefaultInitClosePercent 以支持无库单测。 + /// + [TestClass] + public class InitUnwindDefaultClosePercentTest + { + // ================================================================ + // 场景1:未平仓 PosiNotionalValue == NotionalValue → ClosePercent = 1 + // ================================================================ + [TestMethod] + public void 未平仓_剩余等于期初_默认ClosePercent为1() + { + var result = SwapDealService.CalcDefaultInitClosePercent( + notionalValue: 1_000_000m, posiNotionalValue: 1_000_000m); + + Assert.AreEqual(1m, result, "未平仓:默认应平 100%(占期初)"); + } + + // ================================================================ + // 场景2:已平 30%(剩 70%)→ ClosePercent = 0.7 + // ================================================================ + [TestMethod] + public void 已平30_剩70_默认ClosePercent为0_7() + { + var result = SwapDealService.CalcDefaultInitClosePercent( + notionalValue: 1_000_000m, posiNotionalValue: 700_000m); + + Assert.AreEqual(0.7m, result, 0.0001m, "已平 30% 剩 70%:默认 ClosePercent=0.7(占期初)"); + } + + // ================================================================ + // 场景3:除零保护 NotionalValue = 0 → 返回 1(容错) + // ================================================================ + [TestMethod] + public void 期初名义本金为零_返回1_容错不除零() + { + var result = SwapDealService.CalcDefaultInitClosePercent( + notionalValue: 0m, posiNotionalValue: 100_000m); + + Assert.AreEqual(1m, result, "期初本金为 0 时容错返回 1,不应抛除零异常"); + } + + // ================================================================ + // 场景4:GLMS-20260701-0013 真实快照(已平 2 次) + // 期初 NotionalValue = 980,000 / 剩余 PosiNotionalValue = 686,000.07 + // 期望 ClosePercent ≈ 0.7(686000.07/980000) + // ================================================================ + [TestMethod] + public void GLMS20260701_0013_已平两次_默认ClosePercent约为0_7() + { + var result = SwapDealService.CalcDefaultInitClosePercent( + notionalValue: 980_000m, posiNotionalValue: 686_000.07m); + + // 686000.07 / 980000 = 0.700000071... + Assert.AreEqual(0.7m, result, 0.0001m, + "GLMS-20260701-0013 已平两次:默认 ClosePercent 应≈0.7(占期初),旧 bug 会硬编码 1"); + } + + // ================================================================ + // 场景5:与 ToRemainingClosePercent 联动验证 + // 前端拿 InitUnwind 返回的 A(占期初) 默认值,经 ToRemainingClosePercent 转 B(占剩余), + // 应恰好 = 1.0(因为"平剩余全部"在占剩余语义下就是 100%) + // ================================================================ + [TestMethod] + public void 默认A经ToRemainingClosePercent转B应为1_平剩余全部() + { + const decimal notionalValue = 1_000_000m; + const decimal posiNotionalValue = 600_000m; // 已平 40%,剩 60% + + var defaultA = SwapDealService.CalcDefaultInitClosePercent(notionalValue, posiNotionalValue); + var convertedB = SwapDealService.ToRemainingClosePercent(defaultA, notionalValue, posiNotionalValue); + + Assert.AreEqual(0.6m, defaultA, 0.0001m, "占期初默认 A=0.6"); + Assert.AreEqual(1.0m, convertedB, 0.0001m, + "A=0.6 经 ToRemainingClosePercent 转换 → B=1.0(占剩余 100% = 平剩余全部),此为占期初/占剩余双语义自洽的关键不变式"); + } + + // ================================================================ + // 场景6:全平完(PosiNotionalValue=0)→ ClosePercent=0(边界,实际不会进 InitUnwind) + // ================================================================ + [TestMethod] + public void 全平完剩余为零_ClosePercent为零() + { + var result = SwapDealService.CalcDefaultInitClosePercent( + notionalValue: 1_000_000m, posiNotionalValue: 0m); + + Assert.AreEqual(0m, result, "剩余本金为 0 时 ClosePercent=0(边界场景,实际全部平完不会再进 InitUnwind)"); + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index f27ec368..75f98fbd 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -267,9 +267,7 @@ namespace YLErp.Modules.SwapModule // 占期初(original)语义(A):默认"平掉剩余全部持仓" = 剩余名义本金/期初名义本金。 // 未平仓时 PosiNotionalValue==NotionalValue → 1(平100%);部分平仓后自动变为剩余比例(如已平10%则默认90%)。 // 与互换/提前终止 InitIncome(L447) 保持一致。 - unwindData.ClosePercent = unwindData.NotionalValue > 0 - ? unwindData.PosiNotionalValue / unwindData.NotionalValue - : 1; + unwindData.ClosePercent = CalcDefaultInitClosePercent(unwindData.NotionalValue, unwindData.PosiNotionalValue); unwindData.CloseNotionalValue = unwindData.PosiNotionalValue; unwindData.CloseQty = unwindData.PositionQty; if (position != null) @@ -755,6 +753,18 @@ namespace YLErp.Modules.SwapModule return remainingClosePercent * posiNotionalValue / notionalValue; } + /// + /// 计算 InitUnwind 默认占期初(A)平仓比例 = "平掉剩余全部持仓"对应的占期初比例。 + /// 即:ClosePercent(A) = PosiNotionalValue / NotionalValue。 + /// 未平仓时 PosiNotionalValue==NotionalValue → 1(平100%); + /// 部分平仓后自动变为剩余比例(如已平 30% 则默认 0.7)。 + /// 与互换/提前终止 InitIncome 保持一致。抽出为纯函数以支持无库单测。 + /// + public static decimal CalcDefaultInitClosePercent(decimal notionalValue, decimal posiNotionalValue) + { + return notionalValue > 0 ? posiNotionalValue / notionalValue : 1; + } + /// /// 获取固定利率 ///