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;
}
}