From 1eb791381a3f3098b96ae0a2b96e228e55160ed1 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 1 Jul 2026 13:02:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(swap):=20=E5=8D=95=E5=88=A9=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=89=A3=E9=99=A4=E5=8E=86=E5=8F=B2=E5=B7=B2=E7=BB=93?= =?UTF-8?q?=E5=88=A9=E6=81=AFconsumedInterest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复利路径已由c6adb3bb修复(加consumedInterest扣除),单利路径 CalcDailySimpleInterest未同步。互换结清后再平仓默认值偏大。 改动(SwapDealService.cs 3处): - cs:437 去掉InterestType==复利条件,单利也取GetConsumedInterest - cs:707 单利调用传入consumedInterest参数 - cs:802/856 CalcDailySimpleInterest签名加consumedInterest+末尾扣除 验证(Step1_SimpleInterestRedTest): 1813单利样本 34203偏大量8271→0✅, 34204预付金偏大量58→0✅(修正测试口径漏乘ratio) 复利回归(1889): 6-30默认值仍6.44✅无退化 --- .../SwapPartialUnwindInterestDefaultTest.cs | 25 +++++++++++-------- .../Modules/SwapModule/SwapDealService.cs | 14 ++++++----- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs index cec5f921..b90501c7 100644 --- a/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapPartialUnwindInterestDefaultTest.cs @@ -231,7 +231,7 @@ namespace YLErp.Modules.SwapModule // 4. 对每个单利腿,对比"默认值"vs"应计基数(待实现-已结利息)" Console.WriteLine($"\n[3] 单利路径诊断:默认值 vs 应计基数"); - Console.WriteLine($" {"PositionId",10} {"InterestMode",14} {"默认值",14} {"eod待实现IPS",14} {"历史已结CI",14} {"应计(IPS-CI)",14} {"偏大量",14} {"红灯",6}"); + Console.WriteLine($" {"PositionId",10} {"InterestMode",14} {"方向",6} {"默认ClosePnL",14} {"InterestAmt",14} {"eod待实现IPS",14} {"历史已结CI",14} {"ratio",6} {"应计(IPS-CI)",14} {"偏大量",14} {"红灯",6}"); int redCount = 0; foreach (var d in defaults.Where(x => x.InterestDirection > 0)) @@ -248,31 +248,36 @@ namespace YLErp.Modules.SwapModule // 历史已结利息(复利路径用的 GetConsumedInterest,单利路径没用) decimal ci = service.GetConsumedInterest(tradeId, d.PositionId, testDate); - // 应计基数 = 待实现 - 已结(这才是正确的"未实现利息") - decimal expected = ips - ci; + // InterestClosePnL = InterestAmount × interestRatio(方向系数) + // interestRatio = InterestDirection==收取(1) ? 1 : -1 + decimal interestRatio = d.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; + // 应计基数 = (待实现 - 已结) × ratio(与 InterestClosePnL 同口径) + decimal expected = (ips - ci) * interestRatio; decimal actual = d.InterestClosePnL; decimal diff = actual - expected; bool isRed = Math.Abs(ci) > 0.01m && Math.Abs(diff) > Math.Abs(ci) * 0.5m; if (isRed) redCount++; string modeName = ((InterestModeEnum)d.InterestMode).ToString(); - Console.WriteLine($" {d.PositionId,10} {modeName,14} {actual,14:F4} {ips,14:F4} {ci,14:F4} {expected,14:F4} {diff,14:F4} {(isRed ? "⚠红灯" : "绿灯"),6}"); + string dirName = ((SwapDirectionEnum)d.InterestDirection).ToString(); + Console.WriteLine($" {d.PositionId,10} {modeName,14} {dirName,6} {actual,14:F4} {d.InterestAmount,14:F4} {ips,14:F4} {ci,14:F4} {interestRatio,6} {expected,14:F4} {diff,14:F4} {(isRed ? "⚠红灯" : "绿灯"),6}"); } Console.WriteLine($"\n[结论]"); if (redCount > 0) { - Console.WriteLine($" ⚠ 坐实单利路径 bug:{redCount} 条单利腿默认值偏大(含历史已结利息)。"); - Console.WriteLine($" 根因:CalcDailySimpleInterest(cs:802) 未加 consumedInterest 扣除(复利 cs:793 已加)。"); + Console.WriteLine($" ⚠ 单利路径仍存在偏大:{redCount} 条单利腿默认值含历史已结利息。"); + Console.WriteLine($" 根因:CalcDailySimpleInterest 未加 consumedInterest 扣除。"); } else { - Console.WriteLine($" 单利路径未检测到偏大(可能已修或样本无历史互换)。"); + Console.WriteLine($" ✅ 单利路径已修复:默认值正确扣除历史已结利息,无偏大。"); } - // 红灯断言:单利路径应存在偏大 - Assert.IsTrue(redCount > 0, - "红灯:单利路径应存在默认值偏大(含历史已结利息)。修复后此断言应反转。"); + // 绿灯断言(修复后):单利路径不应再存在偏大 + Assert.AreEqual(0, redCount, + $"绿灯:单利路径默认值应正确扣除历史已结利息(consumedInterest)," + + $"但仍有 {redCount} 条腿偏大。"); } finally { diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index a8ee764f..bdac9b16 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -433,10 +433,8 @@ namespace YLErp.Modules.SwapModule else { // 盘中互换场景,使用 CalcUnwindInterest - // 取历史已结利息(事件级,互换当时落库),供复利重算扣除(仅复利需要;单利基于日终快照自带状态) - var consumedInterest = position.InterestType == (int)InterestTypeEnum.复利 - ? GetConsumedInterest(td.id, position.id, endDate) - : 0m; + // 取历史已结利息(事件级,互换当时落库),供单利/复利重算时扣除已通过互换结出的利息,避免重复计入 + var consumedInterest = GetConsumedInterest(td.id, position.id, endDate); interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal, closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst, calcLast||newCalcLast, consumedInterest)); } } @@ -704,7 +702,7 @@ namespace YLErp.Modules.SwapModule } else { - CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); + CalcDailySimpleInterest(preEodPosition, endDate, position, posiNotionalValue, interest, annualDays, needPrice, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest); } interest.InterestAmount = Math.Round(InterestAmount, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); @@ -799,7 +797,7 @@ namespace YLErp.Modules.SwapModule /// /// 计算单利 盘中(按重置天数分段,每段使用对应浮动利率) /// - public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, 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) { var startDate = position.PosiStartDate; decimal interestProfitSum = preEodPosition.InterestProfitSum; @@ -855,6 +853,10 @@ namespace YLErp.Modules.SwapModule tdinterest += tdinterest1; } } + // 单利同样需扣除历史已通过互换结出的利息(与复利 cs:793 对齐), + // 否则部分互换结清后再平仓,默认值会重复计入已实现部分。 + interest -= consumedInterest; + tdinterest -= consumedInterest; InterestAmount = Math.Round(interest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); TdInterestAmount = Math.Round(tdinterest, ConsGlobal.PriceRound, MidpointRounding.AwayFromZero); }