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] 含端点的重置日也生成段