diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs index 9724c4b5..b9dbe757 100644 --- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -1562,6 +1562,9 @@ namespace YLErp.Modules.SwapModule "0005 计算不算尾时,部分平仓日终新增复利必须按剩余70%本金计提"); AssertDecimal(196212.213529615939m, partialEod.InterestIncomeSum, "0005 部分平仓后日终待实现复利必须扣除实际80002.30结算"); + AssertDecimal(previousEod.TdInterestPrincipal * (1m - partialNotional / originalNotional), + partialEod.TdInterestPrincipal, + "0005 部分平仓后,下一日复利本金必须只继承剩余70%本金"); var intermediateDate = new DateTime(2026, 5, 18); var intermediateInterest = dealService.GetInterests( @@ -1573,6 +1576,39 @@ namespace YLErp.Modules.SwapModule Assert.IsTrue(Math.Abs(259348.386714765m - intermediateInterest.InterestAmount) <= 0.01m, $"0005 5/18 复利应承接部分平仓后的累计利息 Expected approximately 259348.386714765, Actual: {intermediateInterest.InterestAmount}"); + var intermediateEod = partialEod.Clone(); + intermediateEod.id = 18283; + intermediateEod.ValueDate = intermediateDate; + intermediateEod.InterestIncomeSum = intermediateInterest.InterestAmount; + intermediateEod.InterestProfitSum = intermediateInterest.InterestAmount; + intermediateEod.TdInterestPrincipal = remainingNotional; + intermediateEod.PosiNotionalValue = remainingNotional; + + var expectedEndFlow = new swap_flow_event { InterestRate = spread }; + decimal expectedAmountAtEnd = 0m; + decimal expectedTdAmountAtEnd = 0m; + dealService.CalcDailyCompoundInterest( + finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false, + intermediateEod.FloatRate, 1m, originalNotional, true, false, + ref expectedAmountAtEnd, ref expectedTdAmountAtEnd); + var expectedPreviousFlow = new swap_flow_event { InterestRate = spread }; + decimal expectedAmountAtPreviousEod = 0m; + decimal expectedTdAmountAtPreviousEod = 0m; + dealService.CalcDailyCompoundInterest( + intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, false, + intermediateEod.FloatRate, 1m, originalNotional, true, true, + ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod); + var expectedFinalInterest = intermediateEod.InterestIncomeSum + + expectedAmountAtEnd - expectedAmountAtPreviousEod; + var finalInterest = dealService.GetInterests( + td, td.trade_extend, finalCloseDate, finalCloseDate, + new List { intermediateEod }, new List { position }, + remainingNotional, remainingNotional, 0m, remainingNotional, 1m, + (int)SwapEventTypeEnum.平仓, false, false, 0m, originalNotional, + settment: false, newCalcLast: false).Single(); + AssertDecimal(expectedFinalInterest, finalInterest.InterestAmount, + "0005 最终全平重放时,历史5/18终点必须包含当日利息后再做差额"); + } [TestMethod] diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 9575f2b0..4e5792f4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1243,7 +1243,19 @@ namespace YLErp.Modules.SwapModule var interestAtEnd = new swap_flow_event { InterestRate = rate }; decimal amountAtEnd = 0m; decimal tdAmountAtEnd = 0m; - CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, + // InitInterestDate 在最终日不算尾时会先把 endDate 回拨一天; + // 历史差分的 amountAtEnd 需补回该日,但计算器仍使用交易 calcLast, + // 并将重放日期限制在合约到期日,避免提前全平或超期重复计息。 + var replayEndDate = endDate; + if (!calcLast && endDate < valueDate) + { + replayEndDate = endDate.AddDays(1); + if (replayEndDate > td.ExerciseDate.Value) + { + replayEndDate = td.ExerciseDate.Value; + } + } + CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue, interestAtEnd, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest); var interestAtPreviousEod = new swap_flow_event { InterestRate = rate }; diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index e3122909..f29709fa 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1427,6 +1427,18 @@ namespace YLErp.Modules.SwapModule var fullPrincipal = lastTdInterestPrincipal > 0m ? lastTdInterestPrincipal : oriPosiNotionalValue; + // CalcSwapInterests 的算头不算尾非重置日快速路径会返回上一 EOD 的全额本金; + // 只有该路径需要按剩余比例缩放,避免已平部分进入下一日复利。 + // 重置日动态本金已包含累计复利,calcLast=true 也已返回当前剩余本金, + // 两者都不能再次缩放,否则会把剩余本金二次打折。 + var usesFullPreviousEodPrincipal = !calcLast + && hasPreviousEod + && (valueDate - eodPayPosition.ValueDate).Days == 1 + && (valueDate - position.PosiStartDate).Days % (position.interest_rest_days ?? 1) != 0; + if (usesFullPreviousEodPrincipal) + { + newEodPayPosition.TdInterestPrincipal *= 1m - closePercent; + } var accrualPrincipal = calcLast ? fullPrincipal : fullPrincipal * (1m - closePercent);