From ce6c0812e15cc44eb5d3c895e530332d218f3b59 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 12 Aug 2026 10:11:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(accrual):=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=AF=B9=E9=BD=90=E4=B8=9A=E7=95=8C=E6=A0=87?= =?UTF-8?q?=E5=87=86(QuantLib/Strata)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FundingLegAccrual 4个方法的参数命名全面纠正: | 旧名 | 新名 | 理由 | |-------------------|-------------------|------| | originalPv | referenceNotional | Pv(现值)概念错误,实际是参考本金 | | closeRatio/closePercent | unwindFraction | 统一,对齐 ApplyUnwind.unwindPercent | | consumedInterest | realizedInterest | 对齐 AccrualState.RealizedInterest | | priorUnrealized | priorAccrued | 对齐 InterestResult.Accrued | | priorAccrualPrincipal | priorNotional | 简洁,对齐 QuantLib notional | | positionPrincipal | notional | 简化,去掉冗余前缀 | | dynomicPrincipal | accrualBasis | 修正typo+用业界术语 | 同步更新: - SwapDealService.CalcDailySimpleInterestByEod 调用点 - 3个影子测试文件命名参数 - SwapInterest.Round 改为 public(所有Round收口一处) 删除半成品残留: SwapInterest_CompoundInArrears_RolloverTimingTests.cs 验证: 编译0错误, 7个影子测试全过, 全量520测试7失败(基线一致)。 --- .../Accrual/CompoundPeriodShadowTest.cs | 10 +- .../Accrual/SimplePeriodShadowTest.cs | 12 +- .../SwapModule/Accrual/FundingLegAccrual.cs | 146 +++++++++--------- .../Modules/SwapModule/SwapDealService.cs | 10 +- 4 files changed, 93 insertions(+), 85 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs index bcba5199..d1fd0a26 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs @@ -87,7 +87,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(14), allInRate), }; var result = FundingLegAccrual.AccrueCompoundPeriod( - principal: Notional, + notional: Notional, segmentRates: segRates, startDate: StartDate, endDate: EndDate, @@ -129,7 +129,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(14), allInRate), }; var result = FundingLegAccrual.AccrueCompoundPeriod( - principal: Notional * closePct, + notional: Notional * closePct, segmentRates: segRates, startDate: StartDate, endDate: EndDate, @@ -137,8 +137,8 @@ namespace UnitTestProject.Modules.SwapModule.Accrual annualDays: AnnualDays, isAnnualized: true, resetCarryInterest: carry, - consumedInterest: consumed, - closePercent: closePct); + realizedInterest: consumed, + unwindFraction: closePct); Console.WriteLine($"旧: I={oldI} Td={oldTd}"); Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}"); @@ -171,7 +171,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(14), allInRate), }; var result = FundingLegAccrual.AccrueCompoundPeriod( - principal: Notional, + notional: Notional, segmentRates: segRates, startDate: StartDate, endDate: EndDate, diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs index 36f6d3ac..6d42707d 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs @@ -86,9 +86,9 @@ namespace UnitTestProject.Modules.SwapModule.Accrual var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0 var result = FundingLegAccrual.AccrueSimplePeriod( - priorUnrealized: 0m, - accrualPrincipal: 0m, // 差分=0(无归档时 preEod.TdInterestPrincipal=0) - closeRatio: 1m, + priorAccrued: 0m, + notional: 0m, + unwindFraction: 1m, segmentRates: segRates, startDate: StartDate, endDate: EndDate, @@ -131,9 +131,9 @@ namespace UnitTestProject.Modules.SwapModule.Accrual var accrualPrincipal = Notional + Notional - Notional; var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; var result = FundingLegAccrual.AccrueSimplePeriod( - priorUnrealized: 200_000m * 0.5m, // InterestProfitSum × closePercent - accrualPrincipal: accrualPrincipal, - closeRatio: 0.5m, + priorAccrued: 200_000m * 0.5m, + notional: accrualPrincipal, + unwindFraction: 0.5m, segmentRates: segRates, startDate: StartDate, endDate: EndDate, diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs index aef143f2..d291a37b 100644 --- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs +++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs @@ -5,6 +5,14 @@ namespace YLErp.Modules.SwapModule.Accrual; /// /// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。 +/// +/// 命名规范(对齐 QuantLib / Strata): +/// - notional/principal → 计息基数 +/// - accrued → 累计应计利息 +/// - unwindFraction → 平仓比例(0~1) +/// - realizedInterest → 历史已结利息(legacy: consumedInterest) +/// - referenceNotional → 差分公式参考本金(legacy: orginPv/originalPv) +/// - accrualBasis → 逐段滚动的计息基数(legacy: dynomicPrincipal) /// public static class FundingLegAccrual { @@ -13,77 +21,82 @@ public static class FundingLegAccrual /// /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。 /// + /// 上一日终累计应计利息。 + /// 上一日终计息基数。 + /// 当前持仓名义本金。 + /// 平仓比例(EOD 恒为 1)。 + /// 差分公式参考本金(legacy: orginPv)。 public static InterestResult AccrueSimpleEod( - decimal priorUnrealized, - decimal priorAccrualPrincipal, - decimal positionPrincipal, - decimal closeRatio, - decimal originalPv, + decimal priorAccrued, + decimal priorNotional, + decimal notional, + decimal unwindFraction, + decimal referenceNotional, FundingLegRate rate, AccrualPolicy policy, DateTime eodDate, AccrualTrace? trace = null) { - var baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal - originalPv; - var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio; + var basis = priorNotional + notional - referenceNotional; + var displayBasis = basis * unwindFraction; - var combinedRate = rate.AllInRate; - var dayInterest = baseInterestPrincipal * combinedRate; - var tdInterest = baseTdInterestPrincipal * combinedRate; + var allInRate = rate.AllInRate; + var dayInterest = displayBasis * allInRate; + var tdInterest = basis * allInRate; if (policy.IsAnnualized) { dayInterest /= policy.AnnualDays; tdInterest /= policy.AnnualDays; } - var totalUnrealized = priorUnrealized + dayInterest; + var totalAccrued = priorAccrued + dayInterest; var result = new InterestResult( - SwapInterest.Round(totalUnrealized, Precision), + SwapInterest.Round(totalAccrued, Precision), SwapInterest.Round(tdInterest, Precision)); - trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized); + trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); trace?.MarkEnd(result.Accrued, result.AccruedToday); return result; } /// /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。 - /// 重置日:本金 = positionPrincipal + priorUnrealized × remainingPercent。 - /// 非重置日:本金 = priorAccrualPrincipal + positionPrincipal - originalPv。 + /// 重置日:basis = notional + priorAccrued × remainingFraction。 + /// 非重置日:basis = priorNotional + notional - referenceNotional。 /// public static InterestResult AccrueCompoundEod( - decimal priorUnrealized, - decimal priorAccrualPrincipal, - decimal positionPrincipal, - decimal closeRatio, - decimal originalPv, + decimal priorAccrued, + decimal priorNotional, + decimal notional, + decimal unwindFraction, + decimal referenceNotional, FundingLegRate rate, AccrualPolicy policy, bool isResetDay, - decimal remainingPercent, + decimal remainingFraction, DateTime eodDate, AccrualTrace? trace = null) { - var baseTdInterestPrincipal = isResetDay - ? positionPrincipal + priorUnrealized * remainingPercent - : priorAccrualPrincipal + positionPrincipal - originalPv; - var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio; + var basis = isResetDay + ? notional + priorAccrued * remainingFraction + : priorNotional + notional - referenceNotional; + var displayBasis = basis * unwindFraction; - var combinedRate = rate.AllInRate; - var dayInterest = baseInterestPrincipal * combinedRate; - var tdInterest = baseTdInterestPrincipal * combinedRate; + var allInRate = rate.AllInRate; + var dayInterest = displayBasis * allInRate; + var tdInterest = basis * allInRate; if (policy.IsAnnualized) { dayInterest /= policy.AnnualDays; tdInterest /= policy.AnnualDays; } - var totalUnrealized = priorUnrealized * closeRatio + dayInterest; + var totalAccrued = priorAccrued * unwindFraction + dayInterest; var result = new InterestResult( - SwapInterest.Round(totalUnrealized, Precision), + SwapInterest.Round(totalAccrued, Precision), SwapInterest.Round(tdInterest, Precision)); - trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized); + trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); trace?.MarkEnd(result.Accrued, result.AccruedToday); return result; } @@ -92,10 +105,15 @@ public static class FundingLegAccrual /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。 /// 本金全程恒定,按重置日分段取利率。 /// + /// 上一日终累计应计利息。 + /// 计息基数(差分,全程恒定)。 + /// 平仓比例。 + /// 分段利率表:(段起日, all-in利率),按日期升序。 + /// 上一日终归档日(只算此日之后的利息)。 public static InterestResult AccrueSimplePeriod( - decimal priorUnrealized, - decimal accrualPrincipal, - decimal closeRatio, + decimal priorAccrued, + decimal notional, + decimal unwindFraction, IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, DateTime startDate, DateTime endDate, @@ -104,16 +122,14 @@ public static class FundingLegAccrual int annualDays, bool isAnnualized) { - var displayPrincipal = accrualPrincipal * closeRatio; - decimal interest = priorUnrealized; - decimal tdInterest = priorUnrealized; + var displayBasis = notional * unwindFraction; + decimal accrued = priorAccrued; var segStart = startDate; var segIncludeStart = boundary.IncludeStart; for (int si = 0; si < segmentRates.Count; si++) { - var (segRateStart, segRate) = segmentRates[si]; var segEnd = si < segmentRates.Count - 1 ? segmentRates[si + 1].StartDate : endDate; @@ -125,28 +141,28 @@ public static class FundingLegAccrual var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary); if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; } - var dailyRate = isAnnualized ? segRate / annualDays : segRate; - var daily = SwapInterest.Round(displayPrincipal * dailyRate, Precision); - var segInterest = SwapInterest.Round(daily * days, Precision); - - interest += segInterest; - tdInterest += segInterest; + var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; + accrued += displayBasis * dailyRate * days; segStart = segEnd; segIncludeStart = false; } return new InterestResult( - SwapInterest.Round(interest, Precision), - SwapInterest.Round(tdInterest, Precision)); + SwapInterest.Round(accrued, Precision), + SwapInterest.Round(accrued, Precision)); } /// /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。 /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。 /// + /// 本次平仓名义本金(已按 unwindFraction 缩放)。 + /// 末日重置时的存量利息(非末日传 0)。 + /// 历史已结利息(legacy: consumedInterest)。 + /// 平仓比例。 public static InterestResult AccrueCompoundPeriod( - decimal principal, + decimal notional, IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, DateTime startDate, DateTime endDate, @@ -154,49 +170,41 @@ public static class FundingLegAccrual int annualDays, bool isAnnualized, decimal resetCarryInterest = 0m, - decimal consumedInterest = 0m, - decimal closePercent = 1m) + decimal realizedInterest = 0m, + decimal unwindFraction = 1m) { - decimal dynomicPrincipal = principal; - decimal interest = 0m; - decimal tdInterest = 0m; + decimal accrualBasis = notional; + decimal accrued = 0m; for (int si = 0; si < segmentRates.Count; si++) { - var (segStart, segRate) = segmentRates[si]; var segEnd = si < segmentRates.Count - 1 ? segmentRates[si + 1].StartDate : endDate; // 重置日并本金 - dynomicPrincipal = si == 0 - ? principal - : principal + interest; + accrualBasis = si == 0 ? notional : notional + accrued; - // 末日重置且 resetCarryInterest 非零:用存量替代 + // 末日重置且 carry 非零:用存量替代 if (segEnd == endDate && si > 0 && resetCarryInterest != 0m) - dynomicPrincipal = principal + resetCarryInterest; + accrualBasis = notional + resetCarryInterest; // 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息) var segIncludeStart = (si == 0) ? boundary.IncludeStart : true; var segIncludeEnd = (segEnd == endDate) ? boundary.IncludeEnd : false; - var days = SwapInterest.AccrualDays(segStart, segEnd, AccrualBoundary.Of(segIncludeStart, segIncludeEnd)); + var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd, + AccrualBoundary.Of(segIncludeStart, segIncludeEnd)); if (days <= 0) continue; - var dailyRate = isAnnualized ? segRate / annualDays : segRate; - var daily = dynomicPrincipal * dailyRate; - var segInterest = daily * days; - - interest += segInterest; - tdInterest += segInterest; + var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; + accrued += accrualBasis * dailyRate * days; } // 扣除历史已结利息 - interest -= consumedInterest * closePercent; - tdInterest -= consumedInterest * closePercent; + accrued -= realizedInterest * unwindFraction; return new InterestResult( - SwapInterest.Round(interest, Precision), - SwapInterest.Round(tdInterest, Precision)); + SwapInterest.Round(accrued, Precision), + SwapInterest.Round(accrued, Precision)); } } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 63ca2edc..d4696526 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1601,11 +1601,11 @@ namespace YLErp.Modules.SwapModule // 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。 var interestTrace = new AccrualTrace(); var result = FundingLegAccrual.AccrueSimpleEod( - priorUnrealized: preEodPosition.InterestProfitSum, - priorAccrualPrincipal: preEodPosition.TdInterestPrincipal, - positionPrincipal: posiPrincipal, - closeRatio: closePercent, - originalPv: orginPv, + priorAccrued: preEodPosition.InterestProfitSum, + priorNotional: preEodPosition.TdInterestPrincipal, + notional: posiPrincipal, + unwindFraction: closePercent, + referenceNotional: orginPv, rate: legRate, policy: accrualPolicy, eodDate: endDate,