Files
zszq-trs/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
T
hjhan e2431e9d44 refactor(accrual): 计息类型整体迁入 DAL——新增 Accrual/InterestMath,删 Core 未接线孤儿
搬迁(算法体逐字未动,仅换命名空间与归属):
- SwapInterest.Round/AccrualDays/FundingLegPrecision + AccrualBoundary/InterestResult
  → YLErpDAL/Modules/SwapModule/Accrual/InterestMath.cs
- AccrualTrace → Accrual/AccrualTrace.cs(被迫同迁:其 MarkStart 引用 AccrualBoundary,
  Core 不能反向依赖 DAL)
- 引用切换:Simple/CompoundInterestAccrual、AccrualPolicy、SwapCalcTrace、SwapDealService
  (保留 using YLErp.Derivatives.Interest——IIndexFixer/IndexFixerBase 留 Core)

删除(零生产引用,孤儿清零):
- Core:SwapInterest.cs 算法方法(AccrueSimple/AccrueCompoundInArrears/ApplyUnwind/
  AccrueUnrealized/ToInterestRate,未接线且与 DAL 生产实现舍入/rollover 口径已分叉)、
  AccrualContext.cs、InterestRate.cs
- DAL:AccrualState.cs(零引用死类)
- 测试:SwapInterest_CompoundInArrears_RolloverTimingTests.cs(仅测已删原语)

验证:两解决方案 Rebuild 0 错误;磁盘 SwapInterest. 残留 0;影子/分红/场景 86/86 通过
(含 Accrual 3 影子对账、Margin 影子、divPower 新增 AutoUnwindMultiPartial)。
注:AccrualContext 默认精度 11 与生产 12 的分叉隐患随删除一并消除;
已删原语若将来重建须先补对账测试,勿凭记忆复原(ARCHITECTURE.md 已留警告)。
2026-08-14 15:31:04 +08:00

130 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 复利计息纯函数——EOD 单日 + intraday 多日。
/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
/// </summary>
public static class CompoundInterestAccrual
{
private const int Precision = InterestMath.FundingLegPrecision;
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
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;
/// <summary>
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
/// </summary>
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;
}
/// <summary>
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
/// </summary>
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;
}
}