diff --git a/YLErpDAL/Modules/SwapModule/Penalty/SwapPenaltyInterestCalculator.cs b/YLErpDAL/Modules/SwapModule/Penalty/SwapPenaltyInterestCalculator.cs index 1f07df45..b85fa4af 100644 --- a/YLErpDAL/Modules/SwapModule/Penalty/SwapPenaltyInterestCalculator.cs +++ b/YLErpDAL/Modules/SwapModule/Penalty/SwapPenaltyInterestCalculator.cs @@ -69,50 +69,13 @@ public static class SwapPenaltyInterestCalculator var boundary = AccrualBoundary.Of(includeStart: !unwindDaySettled, includeEnd: maturityCalcLast); var allInRate = frozenRate.AllInRate; - decimal amount; - decimal finalBasis; - if (policy.IsCompound) - { - // 复利:即使利率冻结为单值,也须按重置日分段(并本金发生在分段边界),每段同一冻结利率。 - // notional = 本金 + 已并入最近重置日的利息:全期轨迹中当前重置段的滚动基数, - // 段内(含罚息窗口首段)每一天都在其上计息——平仓日落在段中间时与全期逐日对齐的关键。 - var segments = BuildFrozenSegments(unwindDate, maturityDate, policy.ResetPeriodDays, resetAnchor, allInRate); - var r = CompoundInterestAccrual.AccruePeriod( - notional: closePrincipal + capitalizedInterest, - segmentRates: segments, - startDate: unwindDate, - endDate: maturityDate, - boundary: boundary, - annualDays: policy.AnnualDays, - isAnnualized: policy.IsAnnualized, - resetCarryInterest: 0m, - realizedInterest: 0m, - unwindFraction: 1m, - finalBasis: out finalBasis, - trace: trace, - carryInInterest: carryInInterest); - amount = r.Accrued; - } - else - { - // 单利:无并本金语义,冻结利率即单段全程 - var r = SimpleInterestAccrual.AccruePeriod( - priorAccrued: 0m, - notional: closePrincipal, - unwindFraction: 1m, - segmentRates: new List<(DateTime StartDate, decimal Rate)> { (unwindDate, allInRate) }, - startDate: unwindDate, - endDate: maturityDate, - priorValueDate: unwindDate.AddDays(-1), - boundary: boundary, - annualDays: policy.AnnualDays, - isAnnualized: policy.IsAnnualized, - trace: trace); - amount = r.Accrued; - finalBasis = closePrincipal; - } + // 按计息方式分派到对应的纯计息路径;二者均产出 (累计利息, 末段计息基数)。 + // 计息数学细节下沉到具名方法,使本方法只表达“分派 + 组装事件”的编排意图,便于阅读与单测。 + var (accrued, finalBasis) = policy.IsCompound + ? AccrueCompound(closePrincipal, capitalizedInterest, carryInInterest, unwindDate, maturityDate, boundary, policy, resetAnchor, allInRate, trace) + : AccrueSimple(closePrincipal, unwindDate, maturityDate, boundary, policy, allInRate, trace); - var rounded = InterestMath.Round(amount, InterestMath.FundingLegPrecision); + var rounded = InterestMath.Round(accrued, InterestMath.FundingLegPrecision); var interest = new swap_flow_event { SwapTradeId = td.id, @@ -137,6 +100,58 @@ public static class SwapPenaltyInterestCalculator return interest; } + /// + /// 复利罚息计息:即使冻结利率为单值,也必须按重置日分段(并本金发生在分段边界),每段同一冻结利率。 + /// notional = 本金 + 已并入最近重置日的利息(capitalizedInterest):全期轨迹中当前重置段的滚动基数, + /// 段内每一天都在其上计息——平仓日落在段中间时与全期逐日对齐的关键。carryInInterest 在首个窗口重置日并入。 + /// 返回 (累计利息, 末段计息基数)。 + /// + private static (decimal Accrued, decimal FinalBasis) AccrueCompound( + decimal closePrincipal, decimal capitalizedInterest, decimal carryInInterest, + DateTime unwindDate, DateTime maturityDate, AccrualBoundary boundary, AccrualPolicy policy, + DateTime resetAnchor, decimal allInRate, AccrualTrace? trace) + { + var segments = BuildFrozenSegments(unwindDate, maturityDate, policy.ResetPeriodDays, resetAnchor, allInRate); + var r = CompoundInterestAccrual.AccruePeriod( + notional: closePrincipal + capitalizedInterest, + segmentRates: segments, + startDate: unwindDate, + endDate: maturityDate, + boundary: boundary, + annualDays: policy.AnnualDays, + isAnnualized: policy.IsAnnualized, + resetCarryInterest: 0m, + realizedInterest: 0m, + unwindFraction: 1m, + finalBasis: out var finalBasis, + trace: trace, + carryInInterest: carryInInterest); + return (r.Accrued, finalBasis); + } + + /// + /// 单利罚息计息:无并本金语义,冻结利率即单段全程。finalBasis 恒为本金(单利不滚基数)。 + /// 返回 (累计利息, 末段计息基数)。 + /// + private static (decimal Accrued, decimal FinalBasis) AccrueSimple( + decimal closePrincipal, DateTime unwindDate, DateTime maturityDate, + AccrualBoundary boundary, AccrualPolicy policy, decimal allInRate, AccrualTrace? trace) + { + var r = SimpleInterestAccrual.AccruePeriod( + priorAccrued: 0m, + notional: closePrincipal, + unwindFraction: 1m, + segmentRates: new List<(DateTime StartDate, decimal Rate)> { (unwindDate, allInRate) }, + startDate: unwindDate, + endDate: maturityDate, + priorValueDate: unwindDate.AddDays(-1), + boundary: boundary, + annualDays: policy.AnnualDays, + isAnnualized: policy.IsAnnualized, + trace: trace); + return (r.Accrued, closePrincipal); + } + /// /// 复利冻结分段:段边界 = 窗口内重置日((d - anchor) % period == 0,对齐 IsResetDay 公式), /// 每段填同一冻结利率。首段必为 (unwindDate, rate);[start,end] 含端点的重置日也生成段