diff --git a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs index 4da4cbb2..0d603c62 100644 --- a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs @@ -369,6 +369,67 @@ namespace YLErp.Modules.SwapModule Assert.IsTrue(Math.Abs((result.FloatRate ?? 0) - (decimal)rateNew) < 0.0001m, $"平仓日=重置日时 FloatRate 应={rateNew}(取到新利率)。" + $"实际={result.FloatRate},若={rateOld} 说明 calcLast=false 跳过了重置日取价(GLMS-JIATT-20260805 根因)"); + + var interestBeforeResetDate = Principal * (FixedRate + (decimal)rateOld) * 7m / AnnualDays; + AssertDecimal(Principal + interestBeforeResetDate, result.InterestPrincipal, + "calcLast=false 的重置日仍应将前 7 天复利并入本金"); + AssertDecimal(interestBeforeResetDate, result.InterestAmount, + "calcLast=false 不应计入重置日当天利息"); + } + + [TestMethod] + public void CI_008_ResetDayPartialCloseCarriesRemainingInterestIntoPrincipal() + { + const decimal previousPrincipal = 50061728.39m; + const decimal remainingPrincipal = 30037037.04m; + const decimal previousInterest = 7425.050203320057m; + const decimal fixedRate = 0.001234m; + const double oldFloatRate = 0.0123; + const double newFloatRate = 0.0213; + var startDate = new DateTime(2026, 7, 28); + var resetDate = new DateTime(2026, 8, 4); + var service = new StubSwapDealService( + new OptUserInfo(0, nameof(ConsumedInterestScenarioTest), OptUserFrom.UnitTest), + d => d >= resetDate ? newFloatRate : oldFloatRate); + var td = CreateTrade(); + td.StartDate = startDate; + td.TradeDate = startDate; + var position = new swap_position + { + id = 1001, SwapTradeId = td.id, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.标的期初全价, + InterestType = (int)InterestTypeEnum.复利, + InterestRateDefault = fixedRate, + PosiStartDate = startDate, PosiMatuirityDate = ExerciseDate, + IsAnnualized = true, interest_rest_days = 7, interest_rule = 0, + FloatRateUnderlyingCode = "FR007", + InterestSwapInterval = JsonConvert.SerializeObject(new List + { + new IntervalModel { Date = ExerciseDate, Rate = fixedRate, Settlement = 0 } + }) + }; + var preEod = new eod_swap_position + { + id = 1, PositionId = position.id, ValueDate = resetDate.AddDays(-1), + TdInterestPrincipal = previousPrincipal, + InterestIncomeSum = previousInterest, + InterestProfitSum = previousInterest, + FloatRate = (decimal)oldFloatRate + }; + + var result = service.GetInterests(td, td.trade_extend, resetDate, resetDate, + new List { preEod }, new List { position }, + remainingPrincipal, remainingPrincipal, 0m, remainingPrincipal, 1m, + (int)SwapEventTypeEnum.平仓, true, false, 0m, remainingPrincipal, + add: false, settment: false, newCalcLast: false).Single(); + + var remainingInterest = previousInterest * remainingPrincipal / previousPrincipal; + var expectedPrincipal = remainingPrincipal + remainingInterest; + var expectedDailyInterest = expectedPrincipal * (fixedRate + (decimal)newFloatRate) / AnnualDays; + AssertDecimal(expectedPrincipal, result.InterestPrincipal); + AssertDecimal(expectedDailyInterest, + result.InterestPrincipal * (result.InterestRate + result.FloatRate.Value) / AnnualDays); } } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7f631a29..42704fed 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1205,7 +1205,14 @@ namespace YLErp.Modules.SwapModule var floateRate = preEodPosition.FloatRate; if (position.InterestType == (int)InterestTypeEnum.复利) { - CalcDailyCompoundInterest( endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest); + var remainingPercent = preEodPosition.TdInterestPrincipal > 0m + ? closePosiNotionalValue / preEodPosition.TdInterestPrincipal + : 1m; + remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent)); + var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent; + CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, + floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, + consumedInterest, resetCarryInterest); } else { @@ -1233,7 +1240,7 @@ namespace YLErp.Modules.SwapModule /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterest( DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m) + public void CalcDailyCompoundInterest(DateTime endDate, swap_position position, decimal principal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m) { var startDate = position.PosiStartDate; decimal interestProfitSum = 0; @@ -1265,15 +1272,14 @@ namespace YLErp.Modules.SwapModule throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fr007RateDate:yyyy年MM月dd日}的价格"); } } - if (!calcFirst && accrueDate == startDate) continue; // 首日不算头 - if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,取价已在上方完成) if (accrueDate >= startDate) { if (i % interestPeriod == 0) { // 复利时:利息并入本金(FR007 取价已提前到 calcFirst/calcLast 跳过之前完成) - dynomicPrincipal = principal + interest; - tdDynomicPrincipal = principal + interest; + var interestToReset = i == 0 || resetCarryInterest == 0m ? interest : resetCarryInterest; + dynomicPrincipal = principal + interestToReset; + tdDynomicPrincipal = principal + interestToReset; flowEvent.InterestPrincipal = tdDynomicPrincipal; TdInterestPrincipal = tdDynomicPrincipal; } @@ -1283,6 +1289,11 @@ namespace YLErp.Modules.SwapModule flowEvent.InterestPrincipal = tdDynomicPrincipal; TdInterestPrincipal = tdDynomicPrincipal; } + } + if (!calcFirst && accrueDate == startDate) continue; // 首日不算头 + if (!calcLast && accrueDate == endDate) continue; // 到期日不算尾(只跳过计息,重置本金已在上方完成) + if (accrueDate >= startDate) + { flowEvent.FloatRate = Convert.ToDecimal(floatRate); var interest1 = flowEvent.InterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); var tdinterest1 = TdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); @@ -1391,7 +1402,11 @@ namespace YLErp.Modules.SwapModule var days = (endDate - tradeDate).Days; if (days % interestPeriod == 0) { - tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum; + var remainingPercent = preEodPosition.TdInterestPrincipal > 0m + ? principal / preEodPosition.TdInterestPrincipal + : 1m; + remainingPercent = Math.Max(0m, Math.Min(1m, remainingPercent)); + tdDynomicPrincipal = tdDynomicPrincipal + interestProfitSum * remainingPercent; if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) { // 获取合适的 rateDate