From d3afa6d27cd9a275e31193bb01f8c73ea3037848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Sat, 8 Aug 2026 17:10:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=E5=90=8E=E5=A4=8D=E5=88=A9=E6=9C=AC?= =?UTF-8?q?=E9=87=91=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91(=E4=BF=AE?= =?UTF-8?q?=E5=A4=8DT+1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复部分平仓后下一日复利本金未按剩余比例继承的问题 - 添加对最终日不算尾时历史差分重放的日期边界控制 - 修复算头不算尾快速路径中已平部分进入下一日复利的错误 - 确保重置日动态本金不会被二次缩放导致剩余本金计算错误 - 添加单元测试验证部分平仓后复利本金按剩余70%本金计提的逻辑 - 补充最终全平重放时历史终点包含当日利息后再做差额的测试用例 --- .../SwapModule/DealInterestsScenarioTest.cs | 36 +++++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 14 +++++++- .../SwapModule/SwapEodPositionService.cs | 12 +++++++ 3 files changed, 61 insertions(+), 1 deletion(-) 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);