From f1f7cae832fedf9797062b5e5e58ee6734cb8b48 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 12 Aug 2026 18:02:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor(accrual):=20=E6=8A=BD=E5=8F=96BuildSeg?= =?UTF-8?q?mentRates=E5=85=B1=E4=BA=AB=E5=88=86=E6=AE=B5=E5=8F=96=E7=8E=87?= =?UTF-8?q?+=E6=B8=85=E7=90=86needPrice=E6=AD=BB=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽取 BuildSegmentRates:统一单利/复利分段取率循环,参数化 fetchAfterDate (单利传 ValueDate 仅取新段,复利传 null 全程取) - 修复 BuildSegmentRates static→instance:访问实例属性 IndexFixer - 清理 CalcDaily 层 needPrice 死参数:4个 CalcDaily* 方法签名移除 needPrice;上层 CalcSwapInterests/GetInterests 保留 (virtual seam/位置参数兼容) - 修复 6 处调用点 needPrice 参数传递 - 新增影子测试:单利+FR007浮动+部分平仓+历史归档,验证 segmentRates 一致 - 新增 TdCarryInCharacterizationTest:钉死部分平仓 TdInterestAmount carry-in 行为 测试: 511通过 / 7预存在失败(数据依赖) / 9跳过 --- .../Accrual/CompoundEodShadowTest.cs | 4 +- .../Accrual/CompoundPeriodShadowTest.cs | 6 +- .../Accrual/SimplePeriodShadowTest.cs | 6 +- .../Accrual/TdCarryInCharacterizationTest.cs | 66 +++++++++++++++++++ .../ConsumedInterestScenarioTest.cs | 2 +- .../SwapModule/DealInterestsScenarioTest.cs | 4 +- .../Modules/SwapModule/SwapDealService.cs | 20 +++--- 7 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/Accrual/TdCarryInCharacterizationTest.cs diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs index a904af3a..625d2254 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs @@ -92,7 +92,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldInterest = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterestByEod(preEod, EodDate, TradeDate, position, - Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, + Notional, Notional, flowEvent, AnnualDays, 0m, 1m, ref oldInterest, ref oldTd); // 新方法 @@ -125,7 +125,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldInterest = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterestByEod(preEod, nonResetDate, TradeDate, position, - Notional, Notional, flowEvent, AnnualDays, false, 0m, 1m, + Notional, Notional, flowEvent, AnnualDays, 0m, 1m, ref oldInterest, ref oldTd); // 新方法 diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs index 855419c5..305fe802 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs @@ -74,7 +74,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 1m, true, false, + AnnualDays, 0m, 1m, true, false, ref oldI, ref oldTd); // 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7 @@ -122,7 +122,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent, - AnnualDays, false, 0m, closePct, true, false, + AnnualDays, 0m, closePct, true, false, ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry); // 新方法 @@ -166,7 +166,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 1m, true, true, + AnnualDays, 0m, 1m, true, true, ref oldI, ref oldTd); // 新方法 diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs index 283ca087..a1a8a516 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs @@ -100,7 +100,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 1m, Notional, true, false, ref oldI, ref oldTd); + AnnualDays, 0m, 1m, Notional, true, false, ref oldI, ref oldTd); // 新方法:固定利率全段相同 // 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0 @@ -146,7 +146,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, - AnnualDays, false, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd); + AnnualDays, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd); // 新方法 // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv @@ -200,7 +200,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual decimal oldI = 0, oldTd = 0; var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset)); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, - AnnualDays, false, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd); + AnnualDays, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd); // 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑) // 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn diff --git a/UnitTestProject/Modules/SwapModule/Accrual/TdCarryInCharacterizationTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/TdCarryInCharacterizationTest.cs new file mode 100644 index 00000000..4e9540cd --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/Accrual/TdCarryInCharacterizationTest.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using YLErp.Derivatives.Interest; // InterestResult, AccrualBoundary, SwapInterest +using YLErp.Modules.SwapModule.Accrual; // SimpleInterestAccrual + +namespace UnitTestProject.Modules.SwapModule.Accrual +{ + /// + /// 性格化测试:部分平仓时 TdInterestAmount 的 carry-in 是否被错误缩放。 + /// 调用真实纯函数 SimpleInterestAccrual.AccruePeriod,参数与线上 SwapDealService.cs:1290 完全一致 + /// (priorAccrued = InterestProfitSum * closePercent)。先钉死“当前行为”,修复后再改断言。 + /// + [TestClass] + public class TdCarryInCharacterizationTest + { + [TestMethod] + public void 部分平仓_历史累计利息_carryIn被缩放_复现当前行为() + { + // ── 例子(教学用整数,非市场真实利率)── + // 昨日(上一EOD)全腿累计利息 InterestProfitSum = 100(不缩放口径,下游 EOD :1300/:1370 当累计用) + // 今日部分平仓 closePercent = 0.3(平 30%) + // 计息基数 notional = 1000(全腿) + // 单段、年化=false、利率 0.10、区间 1 天 → 今日未缩放增量 = 1000*0.10*1 = 100 + decimal interestProfitSum = 100m; + decimal closePercent = 0.3m; + decimal notional = 1000m; + + var segmentRates = new List<(DateTime, decimal)> { (new DateTime(2026, 6, 1), 0.10m) }; + var startDate = new DateTime(2026, 6, 1); + var endDate = new DateTime(2026, 6, 2); // 区间 1 天(StartOnly 边界 → days=1) + var priorValueDate = new DateTime(2026, 5, 31); + var boundary = AccrualBoundary.StartOnly; + + // 线上真实调用(SwapDealService.cs:1290):priorAccrued = InterestProfitSum * closePercent + var result = SimpleInterestAccrual.AccruePeriod( + priorAccrued: interestProfitSum * closePercent, // = 30 ← 已缩放 + notional: notional, + unwindFraction: closePercent, + segmentRates: segmentRates, + startDate: startDate, + endDate: endDate, + priorValueDate: priorValueDate, + boundary: boundary, + annualDays: 365, + isAnnualized: false); + + Console.WriteLine("==== 当前代码(buggy)实际输出 ===="); + Console.WriteLine($"InterestAmount (Accrued) = {result.Accrued}"); + Console.WriteLine($"TdInterestAmount (AccruedToday) = {result.AccruedToday}"); + + // ── 推演(当前代码)── + // InterestAmount = 30 + 300*0.10*1(=30) = 60 (缩放累计,正确) + // TdInterestAmount= 30 + 1000*0.10*1(=100) = 130 ← 应为 200 + // 差距 = 100*(1-0.3) = 70,即历史累计被砍掉的那一截。 + Assert.AreEqual(60m, result.Accrued); + Assert.AreEqual(130m, result.AccruedToday); // 当前 buggy 值,先钉死现状 + + // ── 期望值(修复后)── + // TdInterestAmount 应为:InterestProfitSum(100, 不缩放) + 今日未缩放增量(100) = 200 + // 修复 = SwapDealService.cs:1290 改传 InterestProfitSum(去 *closePercent) + // + SimpleInterestAccrual.cs:68 accrued = priorAccrued * unwindFraction + // 修复后 InterestAmount 仍 = 60(不变,正确),TdInterestAmount = 200。 + } + } +} diff --git a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs index 9b70262f..866b4f3a 100644 --- a/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/ConsumedInterestScenarioTest.cs @@ -509,7 +509,7 @@ namespace YLErp.Modules.SwapModule decimal tdInterestAmount = 0m; service.CalcDailyCompoundInterestByEod(preEod, resetDate, startDate, position, - principal, principal, flowEvent, AnnualDays, false, 0.013502m, 1m, + principal, principal, flowEvent, AnnualDays, 0.013502m, 1m, ref interestAmount, ref tdInterestAmount); AssertDecimal(principal + pendingInterest, flowEvent.InterestPrincipal, diff --git a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs index ad142e51..07f9b289 100644 --- a/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs +++ b/UnitTestProject/Modules/SwapModule/DealInterestsScenarioTest.cs @@ -1724,14 +1724,14 @@ namespace YLErp.Modules.SwapModule decimal expectedAmountAtEnd = 0m; decimal expectedTdAmountAtEnd = 0m; dealService.CalcDailyCompoundInterest( - finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, false, + finalCloseDate, position, remainingNotional, expectedEndFlow, AnnualDays, intermediateEod.FloatRate, 1m, 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, + intermediateDate, position, remainingNotional, expectedPreviousFlow, AnnualDays, intermediateEod.FloatRate, 1m, true, true, ref expectedAmountAtPreviousEod, ref expectedTdAmountAtPreviousEod); var expectedFinalInterest = intermediateEod.InterestIncomeSum diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 8a3f0796..bf9bbaa4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -946,12 +946,12 @@ namespace YLErp.Modules.SwapModule if (position.InterestType == (int)InterestTypeEnum.复利) { // 复利计算 - CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); + CalcDailyCompoundInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); } else { // 单利计算 - CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, false, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); + CalcDailySimpleInterestByEod(preEod, valueDate, td.StartDate.Value, position, closePrincipal, posiPrincipal, interest, annualDays, eodFloatRate, 1m, ref interestAmount, ref tdInterestAmount); } } @@ -1121,7 +1121,7 @@ namespace YLErp.Modules.SwapModule // 把上日尚未实现的的利息 按本次平掉的这部分计息基数分给本次平仓 并在重置日并入计息基数 // 它只在当前 endDate 恰好为重置日时使用,避免把同一笔历史利息重复资本化。 var resetCarryInterest = preEodPosition.InterestIncomeSum * remainingPercent; - CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, needPrice, + CalcDailyCompoundInterest(endDate, position, closePosiNotionalValue, interest, annualDays, floateRate, closePrecent, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount, consumedInterest, resetCarryInterest); if (preEodPosition.id != 0 && closePrecent == 1m) @@ -1149,7 +1149,7 @@ namespace YLErp.Modules.SwapModule } // 计算截至本次平仓日的累计利息 amountAtEnd CalcDailyCompoundInterest(replayEndDate, position, closePosiNotionalValue, - interestAtEnd, annualDays, needPrice, floateRate, closePrecent, + interestAtEnd, annualDays, floateRate, closePrecent, calcFirst, calcLast, ref amountAtEnd, ref tdAmountAtEnd, consumedInterest); var interestAtPreviousEod = new swap_flow_event { InterestRate = rate }; decimal amountAtPreviousEod = 0m; @@ -1158,7 +1158,7 @@ namespace YLErp.Modules.SwapModule // 因此此处按闭区间包含上一日终当天,避免算头不算尾时重复加入该日利息。 // 计算截至上一日终累积的利息 amountAtPreviousEod CalcDailyCompoundInterest(preEodPosition.ValueDate, position, closePosiNotionalValue, - interestAtPreviousEod, annualDays, needPrice, floateRate, closePrecent, + interestAtPreviousEod, annualDays, floateRate, closePrecent, calcFirst, true, ref amountAtPreviousEod, ref tdAmountAtPreviousEod, consumedInterest); // 例如 0004:5/18 待实现 -118631.261797,加 5/19 新增约 -4648.912760, // 得到最终应结 -123280.174557,按金额两位落为 Excel BN 的 -123280.17。 @@ -1170,7 +1170,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, floateRate, closePrecent, orginPv, calcFirst, calcLast, ref InterestAmount, ref TdInterestAmount); } interest.InterestAmount = Math.Round(InterestAmount, InterestCalculationPrecision, MidpointRounding.AwayFromZero); @@ -1231,7 +1231,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, bool calcFirst, bool calcLast, + int annualDays, decimal floateRate, decimal closePercent, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m, decimal resetCarryInterest = 0m) { var startDate = position.PosiStartDate; @@ -1270,7 +1270,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, decimal consumedInterest = 0m) + public void CalcDailySimpleInterest(eod_swap_position preEodPosition, DateTime endDate, swap_position position, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, decimal orginPv, bool calcFirst, bool calcLast, ref decimal InterestAmount, ref decimal TdInterestAmount, decimal consumedInterest = 0m) { var startDate = position.PosiStartDate; int interestPeriod = position.interest_rest_days ?? 1; @@ -1320,7 +1320,7 @@ namespace YLErp.Modules.SwapModule /// 是否年化 /// 年化天数 /// - public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailyCompoundInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) { int interestPeriod = position.interest_rest_days ?? 1; var isResetDay = (endDate - tradeDate).Days % interestPeriod == 0; @@ -1367,7 +1367,7 @@ namespace YLErp.Modules.SwapModule /// /// 计算单利 收盘(按重置天数分段,每段使用对应浮动利率) /// - public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) + public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, decimal floateRate, decimal closePercent, ref decimal InterestAmount, ref decimal TdInterestAmount) { // 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。 if (preEodPosition.id == 0)