搬迁(算法体逐字未动,仅换命名空间与归属): - 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 已留警告)。
130 lines
5.4 KiB
C#
130 lines
5.4 KiB
C#
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;
|
||
}
|
||
}
|