namespace YLErp.Modules.SwapModule.Accrual; /// /// 复利计息纯函数——EOD 单日 + intraday 多日。 /// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。 /// public static class CompoundInterestAccrual { private const int Precision = InterestMath.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( InterestMath.Round(totalAccrued, Precision), InterestMath.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 = InterestMath.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( InterestMath.Round(accrued, Precision), InterestMath.Round(accrued, Precision)); trace?.MarkEnd(result.Accrued, result.AccruedToday); return result; } }