diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
index ae3c5e42..1631b512 100644
--- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
+++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs
@@ -422,6 +422,174 @@ namespace YLErp.Modules.SwapModule
Console.WriteLine($"连续5天归档:InterestIncomeSum 从0递增到{runningIncomeSum:F6} = 5×{DailyInterest:F6} ✅");
}
+ // ================================================================
+ // 场景4:互换→收盘→再攒→再互换 守恒验证
+ // ================================================================
+
+ #region 场景4:多次互换结算守恒
+
+ ///
+ /// [DI_SWAP_MULTI_001] 攒10天→互换结清→再攒5天→再互换结清
+ /// ---------------------------------------------------------------
+ /// 验证:第一次互换后 InterestIncomeSum≈当天新计(攒的10天付了),
+ /// 再攒5天后 InterestIncomeSum≈6天(5天新攒+1天当天新计),
+ /// 第二次互换后 InterestIncomeSum≈当天新计(攒的6天又付了)。
+ ///
+ /// 守恒约束:两次互换结算的 TdCloseInterest 之和 = 全程利息(15天+2天新计)。
+ ///
+ [TestMethod]
+ public void DI_SWAP_MULTI_001_多次互换结算守恒()
+ {
+ var td = CreateTrade();
+ var position = CreateInterestPosition();
+
+ // --- Phase 1: 攒10天(4/27~5/6),到5/6 ---
+ var date10 = StartDate.AddDays(10); // 5/7
+ decimal sum10days = Math.Round(Principal * FixedRate * 10 / AnnualDays,
+ ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
+
+ var preEod10 = CreatePreEod(date10.AddDays(-1), sum10days - DailyInterest); // 前日=9天
+ // 当天新计让它到10天
+ var svc1 = new StubEodPositionService();
+ svc1.ExecuteDealInterests(new List { position },
+ new List { preEod10 }, date10, td,
+ new List(), Principal, 0m, 0m, 1m, Principal);
+ var eod10days = svc1.PersistedPositions[0];
+ AssertDecimal(sum10days, eod10days.InterestIncomeSum, "10天后待实现应=10天利息");
+ Console.WriteLine($"Phase1: 攒10天 InterestIncomeSum={eod10days.InterestIncomeSum:F6}");
+
+ // --- Phase 2: 5/7 互换结清 ---
+ var swapDate1 = date10; // 同天互换
+ var svc2 = new StubEodPositionService();
+ var swapEvt1 = CreateSwapFlowEvent(swapDate1, sum10days);
+ var swapResult1 = svc2.ExecuteSaveEodInterestPosition(
+ eod10days, null, position, td, swapDate1, new List { swapEvt1 });
+
+ // 互换后待实现≈当天新计(攒的10天付了,但当天又产生1天新计)
+ decimal dailyInc = Math.Round(Principal * FixedRate / AnnualDays,
+ ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
+ AssertDecimal(dailyInc, swapResult1.InterestIncomeSum, "第一次互换后待实现≈当天新计");
+ decimal firstRealized = swapResult1.TdCloseInterest;
+ Console.WriteLine($"Phase2: 第一次互换 TdCloseInterest={firstRealized:F6}, 待实现={swapResult1.InterestIncomeSum:F6}");
+
+ // --- Phase 3: 再攒5天 ---
+ decimal runningSum = swapResult1.InterestIncomeSum;
+ var runningDate = swapDate1.AddDays(1);
+ for (int i = 0; i < 5; i++)
+ {
+ var svc = new StubEodPositionService();
+ var preEod = CreatePreEod(runningDate.AddDays(-1), runningSum);
+ // 需要 preEod.RealizedInterest 累积
+ preEod.RealizedInterest = swapResult1.RealizedInterest;
+ svc.ExecuteDealInterests(new List { position },
+ new List { preEod }, runningDate, td,
+ new List(), Principal, 0m, 0m, 1m, Principal);
+ runningSum = svc.PersistedPositions[0].InterestIncomeSum;
+ runningDate = runningDate.AddDays(1);
+ }
+ Console.WriteLine($"Phase3: 再攒5天后 InterestIncomeSum={runningSum:F6}");
+
+ // --- Phase 4: 再互换结清 ---
+ var svc4 = new StubEodPositionService();
+ var preEodFinal = CreatePreEod(runningDate.AddDays(-1), runningSum);
+ preEodFinal.RealizedInterest = swapResult1.RealizedInterest;
+ var swapEvt2 = CreateSwapFlowEvent(runningDate, runningSum);
+ var swapResult2 = svc4.ExecuteSaveEodInterestPosition(
+ preEodFinal, null, position, td, runningDate, new List { swapEvt2 });
+ decimal secondRealized = swapResult2.TdCloseInterest;
+ Console.WriteLine($"Phase4: 第二次互换 TdCloseInterest={secondRealized:F6}, 待实现={swapResult2.InterestIncomeSum:F6}");
+
+ // 守恒:两次互换实现的 + 最终待实现 = 全程天数 × dailyInc
+ // 全程天数 = 10天(Phase1) + 1天(第一次互换当天新计) + 5天(Phase3) + 1天(第二次互换当天新计) = 17天
+ // 但第一次互换的当天新计进了 InterestIncomeSum 没进 TdCloseInterest,
+ // 第二次互换同理。所以守恒 = RealizedInterest合计 + 最终InterestIncomeSum = 全程利息
+ decimal totalDays = 10 + 1 + 5 + 1; // 17天
+ decimal expectedTotalInterest = Math.Round(Principal * FixedRate * totalDays / AnnualDays,
+ ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
+ decimal actualTotal = swapResult2.RealizedInterest + swapResult2.InterestIncomeSum;
+ Console.WriteLine($"守恒: RealizedInterest({swapResult2.RealizedInterest:F6}) + InterestIncomeSum({swapResult2.InterestIncomeSum:F6}) = {actualTotal:F6}");
+ Console.WriteLine($"期望: {totalDays}天 × {dailyInc:F6} = {expectedTotalInterest:F6}");
+ AssertDecimal(expectedTotalInterest, actualTotal,
+ "已实现+待实现 应=全程利息(守恒)");
+ }
+
+ #endregion
+
+ // ================================================================
+ // 场景5:预付金腿(marginTypes ratio 翻转)符号验证
+ // ================================================================
+
+ #region 场景5:预付金腿 ratio 翻转
+
+ ///
+ /// [DI_MARGIN_001] 预付金腿互换结清后 RealizedInterest 应为负(支付方向)
+ /// ---------------------------------------------------------------
+ /// 预付金腿 InterestDirection=收取(1),但 marginTypes 会把 ratio 翻转为 -1。
+ /// SwapPositionValue 应为负(负债),RealizedInterest 也应为负(券商支付)。
+ /// 验证 cs:789-793 的 ratio 翻转逻辑。
+ /// ---------------------------------------------------------------
+ ///
+ [TestMethod]
+ public void DI_MARGIN_001_预付金腿RealizedInterest为负()
+ {
+ var service = new StubEodPositionService();
+ var td = CreateTrade();
+ var settleDate = new DateTime(2026, 5, 10);
+
+ // 预付金腿(初始预付金 InterestMode=5,InterestDirection=收取)
+ var marginPosition = new swap_position
+ {
+ id = 2001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown,
+ InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.初始预付金,
+ InterestRateDefault = 0.005m, InterestPrincipalFix = 500m,
+ PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate,
+ IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利,
+ IsAnnualized = true, interest_rest_days = 1, interest_rule = 0,
+ FloatRateUnderlyingCode = null,
+ InterestSwapInterval = null
+ };
+
+ // 攒10天的预付金利息
+ decimal marginDaily = Math.Round(500m * 0.005m / AnnualDays,
+ ConsGlobal.PriceRound, MidpointRounding.AwayFromZero);
+ decimal margin10days = marginDaily * 10;
+ var preEod = new eod_swap_position
+ {
+ id = 200, SwapTradeId = 1, PositionId = 2001, ValueDate = settleDate.AddDays(-1),
+ ClientId = 999998, InterestDirection = (int)SwapDirectionEnum.收取,
+ InterestMode = (int)InterestModeEnum.初始预付金,
+ InterestIncomeSum = margin10days, InterestProfitSum = margin10days,
+ RealizedInterest = 0m, InterestRateDefault = 0.005m,
+ TdInterestPrincipal = 500m, PosiNotionalValue = 500m,
+ InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true,
+ interest_rest_days = 1, FloatRate = 0m
+ };
+
+ // 互换结清
+ var swapEvent = new swap_flow_event
+ {
+ id = 3001, SwapTradeId = 1, EventType = (int)SwapFlowEventTypeEnum.互换,
+ EventDate = settleDate, UnwindDate = settleDate, PositionId = 2001,
+ InterestDirection = (int)SwapDirectionEnum.收取,
+ InterestAmount = margin10days, InterestClosePnL = -margin10days, // 预付金 ratio 翻转后为负
+ InterestRate = 0.005m, InterestMode = (int)InterestModeEnum.初始预付金,
+ InterestPrincipal = 500m, FloatRate = 0m,
+ DataState = (int)SwapFlowDateStateEnum.完成
+ };
+
+ var result = service.ExecuteSaveEodInterestPosition(
+ preEod, null, marginPosition, td, settleDate, new List { swapEvent });
+
+ // 预付金 marginTypes 翻转 ratio=-1
+ // RealizedInterest = 0 + TdCloseInterest(margin10days) * ratio(-1) = -margin10days
+ AssertDecimal(-margin10days, result.RealizedInterest,
+ "预付金腿 RealizedInterest 应为负(ratio翻转后支付方向)");
+ Console.WriteLine($"预付金腿 RealizedInterest={result.RealizedInterest:F6}(负=支付)✅");
+ Console.WriteLine($"SwapPositionValue={result.SwapPositionValue:F6}(应≈当天新计×ratio=-正)");
+ }
+
+ #endregion
+
#endregion
}
}