Files
zszq-trs/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
T

142 lines
6.3 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>
/// <param name="carryInInterest">
/// 窗口前已计未结转利息(EQD-6977 罚息承接):在【首个重置日】并入计息基数——
/// 与"持有至到期"全期轨迹严格对齐(恒等式:全期复利 = 平仓日已结利息 + 罚息窗口利息)。
/// 默认 0 时与旧行为逐位一致。与 resetCarryInterest 互斥使用(后者是全平重放的末段存量替代)。
/// </param>
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 carryInInterest = 0m)
{
decimal accrualBasis = notional;
// carryInInterest 是"窗口前已计未结转利息":先并入 accrued,随首个重置日的
// basis = notional + accrued 一并资本化,并在其后每个重置日持续留在基数里
//(与全窗口重放时 accrued 含全部历史利息的轨迹严格一致);最终报告时扣除。
decimal accrued = carryInInterest;
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;
// 报告口径只含窗口内增量(carryIn 是窗口前已结利息,由正常平仓流单独结算)
accrued -= carryInInterest;
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;
}
}