From 9d8cfb5ad0e6126dabdedf27e15efd7bb164785d Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 12 Aug 2026 13:40:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor(accrual):=20=E6=8B=86=E5=88=86FundingL?= =?UTF-8?q?egAccrual=E2=86=92SimpleInterestAccrual/CompoundInterestAccrual?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单利与复利语义完全不同(单利本金恒定/复利重置日并本金), 拆成两个独立静态类,各自只含自己的方法: SimpleInterestAccrual: - AccrueEod (原AccrueSimpleEod) - AccruePeriod (原AccrueSimplePeriod) CompoundInterestAccrual: - EodBasis (原CompoundEodBasis) - AccrueEod (原AccrueCompoundEod) - AccruePeriod (原AccrueCompoundPeriod) 方法名去掉Simple/Compound前缀(类名已携带类型),消除冗余 SwapModule零回归(7基线/510通过) --- .../Accrual/CompoundEodShadowTest.cs | 6 +- .../Accrual/CompoundPeriodShadowTest.cs | 8 +- .../Accrual/SimplePeriodShadowTest.cs | 8 +- .../SwapUnwindSameDayDoublePartialTest.cs | 2 +- .../Accrual/CompoundInterestAccrual.cs | 132 ++++++++++ .../SwapModule/Accrual/FundingLegAccrual.cs | 239 ------------------ .../Accrual/SimpleInterestAccrual.cs | 106 ++++++++ .../Modules/SwapModule/SwapDealService.cs | 14 +- 8 files changed, 257 insertions(+), 258 deletions(-) create mode 100644 YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs delete mode 100644 YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs create mode 100644 YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs index 4c2af32c..c3b75ee7 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundEodShadowTest.cs @@ -13,7 +13,7 @@ using YLErp.Core.Interest; namespace UnitTestProject.Modules.SwapModule.Accrual { /// - /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。 + /// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。 /// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。 /// [TestClass] @@ -99,7 +99,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual var rate = FundingLegRate.Fixed(FixedRate); var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true); var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional)); - var result = FundingLegAccrual.AccrueCompoundEod( + var result = CompoundInterestAccrual.AccrueEod( 50_000m, Notional, Notional, 1m, rate, policy, isResetDay: true, remainingPercent, EodDate); @@ -131,7 +131,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual // 新方法 var rate = FundingLegRate.Fixed(FixedRate); var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true); - var result = FundingLegAccrual.AccrueCompoundEod( + var result = CompoundInterestAccrual.AccrueEod( 30_000m, Notional, Notional, 1m, rate, policy, isResetDay: false, 0m, nonResetDate); diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs index ce55867b..dad0831c 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundPeriodShadowTest.cs @@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual; namespace UnitTestProject.Modules.SwapModule.Accrual { /// - /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。 + /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。 /// [TestClass] public class CompoundPeriodShadowTest @@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; - var result = FundingLegAccrual.AccrueCompoundPeriod( + var result = CompoundInterestAccrual.AccruePeriod( notional: Notional, segmentRates: segRates, startDate: StartDate, @@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; - var result = FundingLegAccrual.AccrueCompoundPeriod( + var result = CompoundInterestAccrual.AccruePeriod( notional: Notional * closePct, segmentRates: segRates, startDate: StartDate, @@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; - var result = FundingLegAccrual.AccrueCompoundPeriod( + var result = CompoundInterestAccrual.AccruePeriod( notional: Notional, segmentRates: segRates, startDate: StartDate, diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs index d932e035..283ca087 100644 --- a/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs +++ b/UnitTestProject/Modules/SwapModule/Accrual/SimplePeriodShadowTest.cs @@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest; namespace UnitTestProject.Modules.SwapModule.Accrual { /// - /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。 + /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。 /// [TestClass] public class SimplePeriodShadowTest @@ -106,7 +106,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual // 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0 var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0 - var result = FundingLegAccrual.AccrueSimplePeriod( + var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 0m, notional: 0m, unwindFraction: 1m, @@ -152,7 +152,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv var accrualPrincipal = Notional + Notional - Notional; var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; - var result = FundingLegAccrual.AccrueSimplePeriod( + var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 200_000m * 0.5m, notional: accrualPrincipal, unwindFraction: 0.5m, @@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv var accrualPrincipal = Notional + Notional - Notional; - var result = FundingLegAccrual.AccrueSimplePeriod( + var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 200_000m * closePct, notional: accrualPrincipal, unwindFraction: closePct, diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs index e6442d6c..5e18c5d1 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindSameDayDoublePartialTest.cs @@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule /// 【同日多次部分平仓 · unwind 基数滚动表征测试】 /// ============================================================================ /// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional - /// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中 + /// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中 /// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal /// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金) /// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓) diff --git a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs new file mode 100644 index 00000000..9418aa46 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs @@ -0,0 +1,132 @@ +using YLErp.Core.Interest; +using YLErp.Derivatives.Interest; + +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 复利计息纯函数——EOD 单日 + intraday 多日。 +/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。 +/// +public static class CompoundInterestAccrual +{ + private const int Precision = SwapInterest.FundingLegPrecision; + + /// 复利日终计息基数(单一真相源,纯函数与调用方共用): + /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。 + /// remainingFraction 对齐 legacy 钳制到 [0,1]。 + public static decimal EodBasis( + bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional) + => isResetDay + ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction)) + : priorNotional; + + /// + /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。 + /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。 + /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。 + /// + public static InterestResult AccrueEod( + decimal priorAccrued, + decimal priorNotional, + decimal notional, + decimal unwindFraction, + FundingLegRate rate, + AccrualPolicy policy, + bool isResetDay, + decimal remainingFraction, + DateTime eodDate, + AccrualTrace? trace = null) + { + var basis = EodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional); + var displayBasis = basis * unwindFraction; + + var allInRate = rate.AllInRate; + trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction); + var dayInterest = displayBasis * allInRate; + var tdInterest = basis * allInRate; + if (policy.IsAnnualized) + { + dayInterest /= policy.AnnualDays; + tdInterest /= policy.AnnualDays; + } + + var totalAccrued = priorAccrued * unwindFraction + dayInterest; + var result = new InterestResult( + SwapInterest.Round(totalAccrued, Precision), + SwapInterest.Round(tdInterest, Precision)); + + trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); + trace?.MarkEnd(result.Accrued, result.AccruedToday); + return result; + } + + /// + /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。 + /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。 + /// + public static InterestResult AccruePeriod( + decimal notional, + IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, + DateTime startDate, + DateTime endDate, + AccrualBoundary boundary, + int annualDays, + bool isAnnualized, + decimal resetCarryInterest, + decimal realizedInterest, + decimal unwindFraction, + out decimal finalBasis, + AccrualTrace? trace = null) + { + decimal accrualBasis = notional; + decimal accrued = 0m; + + trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized); + + for (int si = 0; si < segmentRates.Count; si++) + { + var isLastSegment = si == segmentRates.Count - 1; + var segEnd = isLastSegment + ? endDate + : segmentRates[si + 1].StartDate; + + // 重置日并本金 + accrualBasis = si == 0 ? notional : notional + accrued; + + // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。 + var usedCarry = false; + if (isLastSegment && si > 0 && resetCarryInterest != 0m + && segmentRates[si].StartDate == endDate) + { + accrualBasis = notional + resetCarryInterest; + usedCarry = true; + } + + if (si > 0) + trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis); + + var segIncludeStart = (si == 0) ? boundary.IncludeStart : true; + var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false; + var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd, + AccrualBoundary.Of(segIncludeStart, segIncludeEnd)); + if (days <= 0) continue; + + var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; + var segInterest = accrualBasis * dailyRate * days; + accrued += segInterest; + trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued); + } + + finalBasis = accrualBasis; + + if (realizedInterest != 0m) + trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction); + accrued -= realizedInterest * unwindFraction; + + var result = new InterestResult( + SwapInterest.Round(accrued, Precision), + SwapInterest.Round(accrued, Precision)); + trace?.MarkEnd(result.Accrued, result.AccruedToday); + return result; + } +} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs deleted file mode 100644 index 90d3500d..00000000 --- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs +++ /dev/null @@ -1,239 +0,0 @@ -using YLErp.Core.Interest; -using YLErp.Derivatives.Interest; - -namespace YLErp.Modules.SwapModule.Accrual; - -/// -/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。 -/// -/// 命名规范(对齐 QuantLib / Strata): -/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准) -/// - accrued → 累计应计利息 -/// - unwindFraction → 平仓比例(0~1) -/// - realizedInterest → 历史已结利息(legacy: consumedInterest) -/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal) -/// -public static class FundingLegAccrual -{ - private const int Precision = SwapInterest.FundingLegPrecision; - - /// 复利日终计息基数(单一真相源,纯函数与调用方共用): - /// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。 - /// remainingFraction 对齐 legacy 钳制到 [0,1]。 - public static decimal CompoundEodBasis( - bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional) - => isResetDay - ? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction)) - : priorNotional; - - /// - /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。 - /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。 - /// - public static InterestResult AccrueSimpleEod( - decimal priorAccrued, - decimal priorNotional, - decimal unwindFraction, - FundingLegRate rate, - AccrualPolicy policy, - DateTime eodDate, - AccrualTrace? trace = null) - { - var basis = priorNotional; - var displayBasis = basis * unwindFraction; - - var allInRate = rate.AllInRate; - var dayInterest = displayBasis * allInRate; - var tdInterest = basis * allInRate; - if (policy.IsAnnualized) - { - dayInterest /= policy.AnnualDays; - tdInterest /= policy.AnnualDays; - } - - var totalAccrued = priorAccrued + dayInterest; - var result = new InterestResult( - SwapInterest.Round(totalAccrued, Precision), - SwapInterest.Round(tdInterest, Precision)); - - trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); - trace?.MarkEnd(result.Accrued, result.AccruedToday); - return result; - } - - /// - /// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。 - /// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。 - /// 非重置日:basis = priorNotional(昨日终滚动计息基数)。 - /// - public static InterestResult AccrueCompoundEod( - decimal priorAccrued, - decimal priorNotional, - decimal notional, - decimal unwindFraction, - FundingLegRate rate, - AccrualPolicy policy, - bool isResetDay, - decimal remainingFraction, - DateTime eodDate, - AccrualTrace? trace = null) - { - var basis = CompoundEodBasis(isResetDay, notional, priorAccrued, remainingFraction, priorNotional); - var displayBasis = basis * unwindFraction; - - var allInRate = rate.AllInRate; - trace?.EodContext(eodDate, isResetDay, unwindFraction, priorAccrued, priorNotional, notional, remainingFraction); - var dayInterest = displayBasis * allInRate; - var tdInterest = basis * allInRate; - if (policy.IsAnnualized) - { - dayInterest /= policy.AnnualDays; - tdInterest /= policy.AnnualDays; - } - - var totalAccrued = priorAccrued * unwindFraction + dayInterest; - var result = new InterestResult( - SwapInterest.Round(totalAccrued, Precision), - SwapInterest.Round(tdInterest, Precision)); - - trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); - trace?.MarkEnd(result.Accrued, result.AccruedToday); - return result; - } - - /// - /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。 - /// 本金全程恒定,按重置日分段取利率。 - /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。 - /// - public static InterestResult AccrueSimplePeriod( - decimal priorAccrued, - decimal notional, - decimal unwindFraction, - IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, - DateTime startDate, - DateTime endDate, - DateTime priorValueDate, - AccrualBoundary boundary, - int annualDays, - bool isAnnualized, - AccrualTrace? trace = null) - { - var displayBasis = notional * unwindFraction; - decimal accrued = priorAccrued; // 缩放累计 → InterestAmount - decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount - - trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized); - - var segStart = startDate; - - for (int si = 0; si < segmentRates.Count; si++) - { - var segEnd = si < segmentRates.Count - 1 - ? segmentRates[si + 1].StartDate - : endDate; - - var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1); - if (effectiveStart > segEnd) { segStart = segEnd; continue; } - - // calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。 - // 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。 - // 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。 - var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true; - // calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾, - // 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。 - var isLastSegment = si == segmentRates.Count - 1; - var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd); - var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary); - if (days <= 0) { segStart = segEnd; continue; } - - var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; - var segInterest = displayBasis * dailyRate * days; - accrued += segInterest; - accruedUnscaled += notional * dailyRate * days; - trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued); - - segStart = segEnd; - } - - var result = new InterestResult( - SwapInterest.Round(accrued, Precision), - SwapInterest.Round(accruedUnscaled, Precision)); - trace?.MarkEnd(result.Accrued, result.AccruedToday); - return result; - } - - /// - /// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。 - /// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。 - /// - public static InterestResult AccrueCompoundPeriod( - decimal notional, - IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, - DateTime startDate, - DateTime endDate, - AccrualBoundary boundary, - int annualDays, - bool isAnnualized, - decimal resetCarryInterest, - decimal realizedInterest, - decimal unwindFraction, - out decimal finalBasis, - AccrualTrace? trace = null) - { - decimal accrualBasis = notional; - decimal accrued = 0m; - - trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized); - - for (int si = 0; si < segmentRates.Count; si++) - { - var isLastSegment = si == segmentRates.Count - 1; - var segEnd = isLastSegment - ? endDate - : segmentRates[si + 1].StartDate; - - // 重置日并本金 - accrualBasis = si == 0 ? notional : notional + accrued; - - // 末日恰好是重置日且 carry 非零:用存量替代(旧代码 i%interestPeriod==0 && accrueDate==endDate)。 - // 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < endDate,不应触发。 - var usedCarry = false; - if (isLastSegment && si > 0 && resetCarryInterest != 0m - && segmentRates[si].StartDate == endDate) - { - accrualBasis = notional + resetCarryInterest; - usedCarry = true; - } - - // 复利每段起点:记录并本金瞬间(非首段 = 利息滚入计息基数) - if (si > 0) - trace?.Rollover(segmentRates[si].StartDate, usedCarry ? resetCarryInterest : accrued, accrualBasis); - - // 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息) - var segIncludeStart = (si == 0) ? boundary.IncludeStart : true; - var segIncludeEnd = isLastSegment ? boundary.IncludeEnd : false; - var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd, - AccrualBoundary.Of(segIncludeStart, segIncludeEnd)); - if (days <= 0) continue; - - var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; - var segInterest = accrualBasis * dailyRate * days; - accrued += segInterest; - trace?.Segment(si, segmentRates[si].StartDate, segEnd, days, segmentRates[si].Rate, accrualBasis, segInterest, accrued); - } - - finalBasis = accrualBasis; - - // 扣除历史已结利息 - if (realizedInterest != 0m) - trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction); - accrued -= realizedInterest * unwindFraction; - - var result = new InterestResult( - SwapInterest.Round(accrued, Precision), - SwapInterest.Round(accrued, Precision)); - trace?.MarkEnd(result.Accrued, result.AccruedToday); - return result; - } -} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs new file mode 100644 index 00000000..fb1378ab --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/SimpleInterestAccrual.cs @@ -0,0 +1,106 @@ +using YLErp.Core.Interest; +using YLErp.Derivatives.Interest; + +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 单利计息纯函数——EOD 单日 + intraday 多日。 +/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。 +/// +public static class SimpleInterestAccrual +{ + private const int Precision = SwapInterest.FundingLegPrecision; + + /// + /// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。 + /// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。 + /// + public static InterestResult AccrueEod( + decimal priorAccrued, + decimal priorNotional, + decimal unwindFraction, + FundingLegRate rate, + AccrualPolicy policy, + DateTime eodDate, + AccrualTrace? trace = null) + { + var basis = priorNotional; + var displayBasis = basis * unwindFraction; + + var allInRate = rate.AllInRate; + var dayInterest = displayBasis * allInRate; + var tdInterest = basis * allInRate; + if (policy.IsAnnualized) + { + dayInterest /= policy.AnnualDays; + tdInterest /= policy.AnnualDays; + } + + var totalAccrued = priorAccrued + dayInterest; + var result = new InterestResult( + SwapInterest.Round(totalAccrued, Precision), + SwapInterest.Round(tdInterest, Precision)); + + trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued); + trace?.MarkEnd(result.Accrued, result.AccruedToday); + return result; + } + + /// + /// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。 + /// 本金全程恒定,按重置日分段取利率。 + /// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。 + /// + public static InterestResult AccruePeriod( + decimal priorAccrued, + decimal notional, + decimal unwindFraction, + IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates, + DateTime startDate, + DateTime endDate, + DateTime priorValueDate, + AccrualBoundary boundary, + int annualDays, + bool isAnnualized, + AccrualTrace? trace = null) + { + var displayBasis = notional * unwindFraction; + decimal accrued = priorAccrued; // 缩放累计 → InterestAmount + decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount + + trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized); + + var segStart = startDate; + + for (int si = 0; si < segmentRates.Count; si++) + { + var segEnd = si < segmentRates.Count - 1 + ? segmentRates[si + 1].StartDate + : endDate; + + var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1); + if (effectiveStart > segEnd) { segStart = segEnd; continue; } + + // calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。 + var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true; + var isLastSegment = si == segmentRates.Count - 1; + var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd); + var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary); + if (days <= 0) { segStart = segEnd; continue; } + + var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate; + var segInterest = displayBasis * dailyRate * days; + accrued += segInterest; + accruedUnscaled += notional * dailyRate * days; + trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued); + + segStart = segEnd; + } + + var result = new InterestResult( + SwapInterest.Round(accrued, Precision), + SwapInterest.Round(accruedUnscaled, Precision)); + trace?.MarkEnd(result.Accrued, result.AccruedToday); + return result; + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 376aa685..154533a3 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1386,7 +1386,7 @@ namespace YLErp.Modules.SwapModule // 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueCompoundPeriod( + var result = CompoundInterestAccrual.AccruePeriod( notional: principal, segmentRates: segmentRates, startDate: startDate, @@ -1428,7 +1428,7 @@ namespace YLErp.Modules.SwapModule // 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount) var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueSimplePeriod( + var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: preEodPosition.InterestProfitSum * closePercent, notional: accrualBasis, unwindFraction: closePercent, @@ -1477,7 +1477,7 @@ namespace YLErp.Modules.SwapModule ? Math.Max(0m, Math.Min(1m, principal / posiPrincipal)) : 1m; - // 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。 + // 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。 var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode); var legRate = isFixedLeg ? FundingLegRate.Fixed(flowEvent.InterestRate) @@ -1491,7 +1491,7 @@ namespace YLErp.Modules.SwapModule // 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。 var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueCompoundEod( + var result = CompoundInterestAccrual.AccrueEod( priorAccrued: preEodPosition.InterestProfitSum, priorNotional: preEodPosition.TdInterestPrincipal, notional: posiPrincipal, @@ -1506,7 +1506,7 @@ namespace YLErp.Modules.SwapModule // flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。 // 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。 - flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis( + flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis( isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction, preEodPosition.TdInterestPrincipal) * closePercent; @@ -1533,7 +1533,7 @@ namespace YLErp.Modules.SwapModule flowEvent.FloatRate = effectiveFloat; - // 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。 + // 纯数学下沉至 SimpleInterestAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。 // 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。 var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode); var legRate = isFixedLeg @@ -1547,7 +1547,7 @@ namespace YLErp.Modules.SwapModule isAnnualized: position.IsAnnualized); // 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。 var interestTrace = new AccrualTrace(); - var result = FundingLegAccrual.AccrueSimpleEod( + var result = SimpleInterestAccrual.AccrueEod( priorAccrued: preEodPosition.InterestProfitSum, priorNotional: preEodPosition.TdInterestPrincipal, unwindFraction: closePercent,