refactor(accrual): 拆分FundingLegAccrual→SimpleInterestAccrual/CompoundInterestAccrual
单利与复利语义完全不同(单利本金恒定/复利重置日并本金), 拆成两个独立静态类,各自只含自己的方法: SimpleInterestAccrual: - AccrueEod (原AccrueSimpleEod) - AccruePeriod (原AccrueSimplePeriod) CompoundInterestAccrual: - EodBasis (原CompoundEodBasis) - AccrueEod (原AccrueCompoundEod) - AccruePeriod (原AccrueCompoundPeriod) 方法名去掉Simple/Compound前缀(类名已携带类型),消除冗余 SwapModule零回归(7基线/510通过)
This commit is contained in:
@@ -13,7 +13,7 @@ using YLErp.Core.Interest;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs FundingLegAccrual.AccrueCompoundEod(新纯函数)。
|
||||
/// 影子测试:CalcDailyCompoundInterestByEod(旧逐日循环)vs CompoundInterestAccrual.AccrueEod(新纯函数)。
|
||||
/// 构造同一组参数,两套实现并行跑,断言结果一致(到分)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
@@ -99,7 +99,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var remainingPercent = Math.Max(0m, Math.Min(1m, Notional / Notional));
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
50_000m, Notional, Notional, 1m, rate, policy,
|
||||
isResetDay: true, remainingPercent, EodDate);
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
// 新方法
|
||||
var rate = FundingLegRate.Fixed(FixedRate);
|
||||
var policy = new AccrualPolicy(AccrualBoundary.Both, true, 7, AnnualDays, true);
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
30_000m, Notional, Notional, 1m, rate, policy,
|
||||
isResetDay: false, 0m, nonResetDate);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using YLErp.Modules.SwapModule.Accrual;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs FundingLegAccrual.AccrueCompoundPeriod(新分段纯函数)。
|
||||
/// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class CompoundPeriodShadowTest
|
||||
@@ -86,7 +86,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
@@ -133,7 +133,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional * closePct,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
@@ -177,7 +177,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
(StartDate.AddDays(7), allInRate),
|
||||
(StartDate.AddDays(14), allInRate),
|
||||
};
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: Notional,
|
||||
segmentRates: segRates,
|
||||
startDate: StartDate,
|
||||
|
||||
@@ -12,7 +12,7 @@ using YLErp.Derivatives.Interest;
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs FundingLegAccrual.AccrueSimplePeriod(新分段纯函数)。
|
||||
/// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SimplePeriodShadowTest
|
||||
@@ -106,7 +106,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
// 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 0m,
|
||||
notional: 0m,
|
||||
unwindFraction: 1m,
|
||||
@@ -152,7 +152,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 200_000m * 0.5m,
|
||||
notional: accrualPrincipal,
|
||||
unwindFraction: 0.5m,
|
||||
@@ -215,7 +215,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
|
||||
// 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv
|
||||
var accrualPrincipal = Notional + Notional - Notional;
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: 200_000m * closePct,
|
||||
notional: accrualPrincipal,
|
||||
unwindFraction: closePct,
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace YLErp.Modules.SwapModule
|
||||
/// 【同日多次部分平仓 · unwind 基数滚动表征测试】
|
||||
/// ============================================================================
|
||||
/// 背景:unwind 计息基数公式 basis = priorNotional + notional - baseNotional
|
||||
/// (FundingLegAccrual / CalcDailyCompoundInterestByEod 同源),其中
|
||||
/// (CompoundInterestAccrual / CalcDailyCompoundInterestByEod 同源),其中
|
||||
/// - priorNotional = 上一日终归档 eod_swap_position.TdInterestPrincipal
|
||||
/// - baseNotional = orginPv = ResolveUnwindPreviousNotional(lastEod)(上一日终浮动端名义本金)
|
||||
/// - notional = 当前持仓名义本金(posiNotionalValue,来自实时持仓)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 复利计息纯函数——EOD 单日 + intraday 多日。
|
||||
/// 复利特征:每个重置日把累计利息并入本金(basis = notional + accrued)。
|
||||
/// </summary>
|
||||
public static class CompoundInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.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(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.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 = SwapInterest.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(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
|
||||
///
|
||||
/// 命名规范(对齐 QuantLib / Strata):
|
||||
/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准)
|
||||
/// - accrued → 累计应计利息
|
||||
/// - unwindFraction → 平仓比例(0~1)
|
||||
/// - realizedInterest → 历史已结利息(legacy: consumedInterest)
|
||||
/// - priorNotional → 昨日终滚动计息基数(legacy: TdInterestPrincipal / dynomicPrincipal)
|
||||
/// </summary>
|
||||
public static class FundingLegAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>复利日终计息基数(单一真相源,纯函数与调用方共用):
|
||||
/// 重置日 = notional + 累计利息×剩余比例(利息并入本金);非重置日 = priorNotional(昨日滚动基数)。
|
||||
/// remainingFraction 对齐 legacy 钳制到 [0,1]。</summary>
|
||||
public static decimal CompoundEodBasis(
|
||||
bool isResetDay, decimal notional, decimal priorAccrued, decimal remainingFraction, decimal priorNotional)
|
||||
=> isResetDay
|
||||
? notional + priorAccrued * Math.Max(0m, Math.Min(1m, remainingFraction))
|
||||
: priorNotional;
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
|
||||
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimpleEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = priorNotional;
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
|
||||
/// 重置日:basis = notional + priorAccrued × remainingFraction(利息并入本金)。
|
||||
/// 非重置日:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
bool isResetDay,
|
||||
decimal remainingFraction,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = CompoundEodBasis(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(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
|
||||
/// 本金全程恒定,按重置日分段取利率。
|
||||
/// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueSimplePeriod(
|
||||
decimal priorAccrued,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
DateTime priorValueDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var displayBasis = notional * unwindFraction;
|
||||
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
|
||||
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
var segStart = startDate;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
|
||||
// 与旧逐日循环一致:if (!calcFirst && accrueDate == startDate) continue 是唯一的首日跳过。
|
||||
// 中间段的 segIncludeStart 被 days<=0 跳过后误置 false,此处按 startDate 判定而非继承标记。
|
||||
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
|
||||
// calcLast 只影响 endDate 本身——只有真正的末段(si==Count-1)才算尾,
|
||||
// 不能用 segEnd==endDate 判断(interestPeriod=1 时中间段 segEnd 也可能==endDate)。
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = displayBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
accruedUnscaled += notional * dailyRate * days;
|
||||
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
|
||||
|
||||
segStart = segEnd;
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
|
||||
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueCompoundPeriod(
|
||||
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)。
|
||||
// 注意:必须同时判断 startDate==endDate——endDate 非重置日时最后一段起点 < 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 = SwapInterest.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(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accrued, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace YLErp.Modules.SwapModule.Accrual;
|
||||
|
||||
/// <summary>
|
||||
/// 单利计息纯函数——EOD 单日 + intraday 多日。
|
||||
/// 单利特征:本金全程恒定(无并本金),按重置日分段取利率。
|
||||
/// </summary>
|
||||
public static class SimpleInterestAccrual
|
||||
{
|
||||
private const int Precision = SwapInterest.FundingLegPrecision;
|
||||
|
||||
/// <summary>
|
||||
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
|
||||
/// EOD 无差分:basis = priorNotional(昨日终滚动计息基数)。
|
||||
/// </summary>
|
||||
public static InterestResult AccrueEod(
|
||||
decimal priorAccrued,
|
||||
decimal priorNotional,
|
||||
decimal unwindFraction,
|
||||
FundingLegRate rate,
|
||||
AccrualPolicy policy,
|
||||
DateTime eodDate,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var basis = priorNotional;
|
||||
var displayBasis = basis * unwindFraction;
|
||||
|
||||
var allInRate = rate.AllInRate;
|
||||
var dayInterest = displayBasis * allInRate;
|
||||
var tdInterest = basis * allInRate;
|
||||
if (policy.IsAnnualized)
|
||||
{
|
||||
dayInterest /= policy.AnnualDays;
|
||||
tdInterest /= policy.AnnualDays;
|
||||
}
|
||||
|
||||
var totalAccrued = priorAccrued + dayInterest;
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(totalAccrued, Precision),
|
||||
SwapInterest.Round(tdInterest, Precision));
|
||||
|
||||
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
|
||||
/// 本金全程恒定,按重置日分段取利率。
|
||||
/// Accrued = 缩放累计(InterestAmount),AccruedToday = 未缩放累计(TdInterestAmount)。
|
||||
/// </summary>
|
||||
public static InterestResult AccruePeriod(
|
||||
decimal priorAccrued,
|
||||
decimal notional,
|
||||
decimal unwindFraction,
|
||||
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
DateTime priorValueDate,
|
||||
AccrualBoundary boundary,
|
||||
int annualDays,
|
||||
bool isAnnualized,
|
||||
AccrualTrace? trace = null)
|
||||
{
|
||||
var displayBasis = notional * unwindFraction;
|
||||
decimal accrued = priorAccrued; // 缩放累计 → InterestAmount
|
||||
decimal accruedUnscaled = priorAccrued; // 未缩放累计 → TdInterestAmount
|
||||
|
||||
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
|
||||
|
||||
var segStart = startDate;
|
||||
|
||||
for (int si = 0; si < segmentRates.Count; si++)
|
||||
{
|
||||
var segEnd = si < segmentRates.Count - 1
|
||||
? segmentRates[si + 1].StartDate
|
||||
: endDate;
|
||||
|
||||
var effectiveStart = segStart > priorValueDate ? segStart : priorValueDate.AddDays(1);
|
||||
if (effectiveStart > segEnd) { segStart = segEnd; continue; }
|
||||
|
||||
// calcFirst 只跳过 startDate 本身;其余天(含重置日、ValueDate+1)只要 > ValueDate 恒纳入。
|
||||
var includeStart = effectiveStart == startDate ? boundary.IncludeStart : true;
|
||||
var isLastSegment = si == segmentRates.Count - 1;
|
||||
var segBoundary = AccrualBoundary.Of(includeStart, isLastSegment && boundary.IncludeEnd);
|
||||
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
|
||||
if (days <= 0) { segStart = segEnd; continue; }
|
||||
|
||||
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
|
||||
var segInterest = displayBasis * dailyRate * days;
|
||||
accrued += segInterest;
|
||||
accruedUnscaled += notional * dailyRate * days;
|
||||
trace?.Segment(si, effectiveStart, segEnd, days, segmentRates[si].Rate, displayBasis, segInterest, accrued);
|
||||
|
||||
segStart = segEnd;
|
||||
}
|
||||
|
||||
var result = new InterestResult(
|
||||
SwapInterest.Round(accrued, Precision),
|
||||
SwapInterest.Round(accruedUnscaled, Precision));
|
||||
trace?.MarkEnd(result.Accrued, result.AccruedToday);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1386,7 +1386,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 纯函数复利计息:分段重置日并本金 + resetCarryInterest + 扣 consumedInterest
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueCompoundPeriod(
|
||||
var result = CompoundInterestAccrual.AccruePeriod(
|
||||
notional: principal,
|
||||
segmentRates: segmentRates,
|
||||
startDate: startDate,
|
||||
@@ -1428,7 +1428,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 纯函数计息:Accrued=缩放累计(InterestAmount),AccruedToday=未缩放累计(TdInterestAmount)
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimplePeriod(
|
||||
var result = SimpleInterestAccrual.AccruePeriod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum * closePercent,
|
||||
notional: accrualBasis,
|
||||
unwindFraction: closePercent,
|
||||
@@ -1477,7 +1477,7 @@ namespace YLErp.Modules.SwapModule
|
||||
? Math.Max(0m, Math.Min(1m, principal / posiPrincipal))
|
||||
: 1m;
|
||||
|
||||
// 纯数学下沉至 FundingLegAccrual.AccrueCompoundEod(DDD 命名 + 末位生产精度 12 舍入)。
|
||||
// 纯数学下沉至 CompoundInterestAccrual.AccrueEod(DDD 命名 + 末位生产精度 12 舍入)。
|
||||
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
|
||||
var legRate = isFixedLeg
|
||||
? FundingLegRate.Fixed(flowEvent.InterestRate)
|
||||
@@ -1491,7 +1491,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 完整计息 trace:前后日期/基数/利率/重置标志全过程,经 SwapCalcTrace 常驻落盘(关键路径日志)。
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueCompoundEod(
|
||||
var result = CompoundInterestAccrual.AccrueEod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum,
|
||||
priorNotional: preEodPosition.TdInterestPrincipal,
|
||||
notional: posiPrincipal,
|
||||
@@ -1506,7 +1506,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// flowEvent.InterestPrincipal:当日计息基数(已按平仓比例缩放)——下游 EOD 用它播种次日 TdInterestPrincipal。
|
||||
// 复用 CompoundEodBasis 单一真相源(与 AccrueCompoundEod 内部同一公式)。
|
||||
flowEvent.InterestPrincipal = FundingLegAccrual.CompoundEodBasis(
|
||||
flowEvent.InterestPrincipal = CompoundInterestAccrual.EodBasis(
|
||||
isResetDay, posiPrincipal, preEodPosition.InterestProfitSum, remainingFraction,
|
||||
preEodPosition.TdInterestPrincipal) * closePercent;
|
||||
|
||||
@@ -1533,7 +1533,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
flowEvent.FloatRate = effectiveFloat;
|
||||
|
||||
// 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
|
||||
// 纯数学下沉至 SimpleInterestAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。
|
||||
// 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。
|
||||
var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode);
|
||||
var legRate = isFixedLeg
|
||||
@@ -1547,7 +1547,7 @@ namespace YLErp.Modules.SwapModule
|
||||
isAnnualized: position.IsAnnualized);
|
||||
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
|
||||
var interestTrace = new AccrualTrace();
|
||||
var result = FundingLegAccrual.AccrueSimpleEod(
|
||||
var result = SimpleInterestAccrual.AccrueEod(
|
||||
priorAccrued: preEodPosition.InterestProfitSum,
|
||||
priorNotional: preEodPosition.TdInterestPrincipal,
|
||||
unwindFraction: closePercent,
|
||||
|
||||
Reference in New Issue
Block a user