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;
+ }
+
///
/// 获取固定利率
///