refactor(accrual): 参数命名对齐业界标准(QuantLib/Strata)

FundingLegAccrual 4个方法的参数命名全面纠正:

| 旧名              | 新名               | 理由 |
|-------------------|-------------------|------|
| originalPv        | referenceNotional | Pv(现值)概念错误,实际是参考本金 |
| closeRatio/closePercent | unwindFraction | 统一,对齐 ApplyUnwind.unwindPercent |
| consumedInterest  | realizedInterest  | 对齐 AccrualState.RealizedInterest |
| priorUnrealized   | priorAccrued      | 对齐 InterestResult.Accrued |
| priorAccrualPrincipal | priorNotional  | 简洁,对齐 QuantLib notional |
| positionPrincipal | notional          | 简化,去掉冗余前缀 |
| dynomicPrincipal  | accrualBasis      | 修正typo+用业界术语 |

同步更新:
- SwapDealService.CalcDailySimpleInterestByEod 调用点
- 3个影子测试文件命名参数
- SwapInterest.Round 改为 public(所有Round收口一处)

删除半成品残留: SwapInterest_CompoundInArrears_RolloverTimingTests.cs

验证: 编译0错误, 7个影子测试全过, 全量520测试7失败(基线一致)。
This commit is contained in:
hjhan
2026-08-12 10:11:26 +08:00
parent 3f194f7ccc
commit ce6c0812e1
4 changed files with 93 additions and 85 deletions
@@ -87,7 +87,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
principal: Notional,
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
@@ -129,7 +129,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
principal: Notional * closePct,
notional: Notional * closePct,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
@@ -137,8 +137,8 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
annualDays: AnnualDays,
isAnnualized: true,
resetCarryInterest: carry,
consumedInterest: consumed,
closePercent: closePct);
realizedInterest: consumed,
unwindFraction: closePct);
Console.WriteLine($"旧: I={oldI} Td={oldTd}");
Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}");
@@ -171,7 +171,7 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
(StartDate.AddDays(14), allInRate),
};
var result = FundingLegAccrual.AccrueCompoundPeriod(
principal: Notional,
notional: Notional,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
@@ -86,9 +86,9 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0
var result = FundingLegAccrual.AccrueSimplePeriod(
priorUnrealized: 0m,
accrualPrincipal: 0m, // 差分=0(无归档时 preEod.TdInterestPrincipal=0)
closeRatio: 1m,
priorAccrued: 0m,
notional: 0m,
unwindFraction: 1m,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
@@ -131,9 +131,9 @@ namespace UnitTestProject.Modules.SwapModule.Accrual
var accrualPrincipal = Notional + Notional - Notional;
var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) };
var result = FundingLegAccrual.AccrueSimplePeriod(
priorUnrealized: 200_000m * 0.5m, // InterestProfitSum × closePercent
accrualPrincipal: accrualPrincipal,
closeRatio: 0.5m,
priorAccrued: 200_000m * 0.5m,
notional: accrualPrincipal,
unwindFraction: 0.5m,
segmentRates: segRates,
startDate: StartDate,
endDate: EndDate,
@@ -5,6 +5,14 @@ namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
///
/// 命名规范(对齐 QuantLib / Strata):
/// - notional/principal → 计息基数
/// - accrued → 累计应计利息
/// - unwindFraction → 平仓比例(0~1
/// - realizedInterest → 历史已结利息(legacy: consumedInterest
/// - referenceNotional → 差分公式参考本金(legacy: orginPv/originalPv
/// - accrualBasis → 逐段滚动的计息基数(legacy: dynomicPrincipal
/// </summary>
public static class FundingLegAccrual
{
@@ -13,77 +21,82 @@ public static class FundingLegAccrual
/// <summary>
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
/// </summary>
/// <param name="priorAccrued">上一日终累计应计利息。</param>
/// <param name="priorNotional">上一日终计息基数。</param>
/// <param name="notional">当前持仓名义本金。</param>
/// <param name="unwindFraction">平仓比例(EOD 恒为 1)。</param>
/// <param name="referenceNotional">差分公式参考本金(legacy: orginPv)。</param>
public static InterestResult AccrueSimpleEod(
decimal priorUnrealized,
decimal priorAccrualPrincipal,
decimal positionPrincipal,
decimal closeRatio,
decimal originalPv,
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
decimal referenceNotional,
FundingLegRate rate,
AccrualPolicy policy,
DateTime eodDate,
AccrualTrace? trace = null)
{
var baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal - originalPv;
var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio;
var basis = priorNotional + notional - referenceNotional;
var displayBasis = basis * unwindFraction;
var combinedRate = rate.AllInRate;
var dayInterest = baseInterestPrincipal * combinedRate;
var tdInterest = baseTdInterestPrincipal * combinedRate;
var allInRate = rate.AllInRate;
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalUnrealized = priorUnrealized + dayInterest;
var totalAccrued = priorAccrued + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalUnrealized, Precision),
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized);
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
/// <summary>
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:本金 = positionPrincipal + priorUnrealized × remainingPercent
/// 非重置日:本金 = priorAccrualPrincipal + positionPrincipal - originalPv
/// 重置日:basis = notional + priorAccrued × remainingFraction
/// 非重置日:basis = priorNotional + notional - referenceNotional。
/// </summary>
public static InterestResult AccrueCompoundEod(
decimal priorUnrealized,
decimal priorAccrualPrincipal,
decimal positionPrincipal,
decimal closeRatio,
decimal originalPv,
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
decimal referenceNotional,
FundingLegRate rate,
AccrualPolicy policy,
bool isResetDay,
decimal remainingPercent,
decimal remainingFraction,
DateTime eodDate,
AccrualTrace? trace = null)
{
var baseTdInterestPrincipal = isResetDay
? positionPrincipal + priorUnrealized * remainingPercent
: priorAccrualPrincipal + positionPrincipal - originalPv;
var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio;
var basis = isResetDay
? notional + priorAccrued * remainingFraction
: priorNotional + notional - referenceNotional;
var displayBasis = basis * unwindFraction;
var combinedRate = rate.AllInRate;
var dayInterest = baseInterestPrincipal * combinedRate;
var tdInterest = baseTdInterestPrincipal * combinedRate;
var allInRate = rate.AllInRate;
var dayInterest = displayBasis * allInRate;
var tdInterest = basis * allInRate;
if (policy.IsAnnualized)
{
dayInterest /= policy.AnnualDays;
tdInterest /= policy.AnnualDays;
}
var totalUnrealized = priorUnrealized * closeRatio + dayInterest;
var totalAccrued = priorAccrued * unwindFraction + dayInterest;
var result = new InterestResult(
SwapInterest.Round(totalUnrealized, Precision),
SwapInterest.Round(totalAccrued, Precision),
SwapInterest.Round(tdInterest, Precision));
trace?.Day(0, eodDate, combinedRate, baseInterestPrincipal, dayInterest, totalUnrealized);
trace?.Day(0, eodDate, allInRate, displayBasis, dayInterest, totalAccrued);
trace?.MarkEnd(result.Accrued, result.AccruedToday);
return result;
}
@@ -92,10 +105,15 @@ public static class FundingLegAccrual
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
/// 本金全程恒定,按重置日分段取利率。
/// </summary>
/// <param name="priorAccrued">上一日终累计应计利息。</param>
/// <param name="notional">计息基数(差分,全程恒定)。</param>
/// <param name="unwindFraction">平仓比例。</param>
/// <param name="segmentRates">分段利率表:(段起日, all-in利率),按日期升序。</param>
/// <param name="priorValueDate">上一日终归档日(只算此日之后的利息)。</param>
public static InterestResult AccrueSimplePeriod(
decimal priorUnrealized,
decimal accrualPrincipal,
decimal closeRatio,
decimal priorAccrued,
decimal notional,
decimal unwindFraction,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
@@ -104,16 +122,14 @@ public static class FundingLegAccrual
int annualDays,
bool isAnnualized)
{
var displayPrincipal = accrualPrincipal * closeRatio;
decimal interest = priorUnrealized;
decimal tdInterest = priorUnrealized;
var displayBasis = notional * unwindFraction;
decimal accrued = priorAccrued;
var segStart = startDate;
var segIncludeStart = boundary.IncludeStart;
for (int si = 0; si < segmentRates.Count; si++)
{
var (segRateStart, segRate) = segmentRates[si];
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
@@ -125,28 +141,28 @@ public static class FundingLegAccrual
var days = SwapInterest.AccrualDays(effectiveStart, segEnd, segBoundary);
if (days <= 0) { segStart = segEnd; segIncludeStart = false; continue; }
var dailyRate = isAnnualized ? segRate / annualDays : segRate;
var daily = SwapInterest.Round(displayPrincipal * dailyRate, Precision);
var segInterest = SwapInterest.Round(daily * days, Precision);
interest += segInterest;
tdInterest += segInterest;
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
accrued += displayBasis * dailyRate * days;
segStart = segEnd;
segIncludeStart = false;
}
return new InterestResult(
SwapInterest.Round(interest, Precision),
SwapInterest.Round(tdInterest, Precision));
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accrued, Precision));
}
/// <summary>
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
/// </summary>
/// <param name="notional">本次平仓名义本金(已按 unwindFraction 缩放)。</param>
/// <param name="resetCarryInterest">末日重置时的存量利息(非末日传 0)。</param>
/// <param name="realizedInterest">历史已结利息(legacy: consumedInterest)。</param>
/// <param name="unwindFraction">平仓比例。</param>
public static InterestResult AccrueCompoundPeriod(
decimal principal,
decimal notional,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
DateTime startDate,
DateTime endDate,
@@ -154,49 +170,41 @@ public static class FundingLegAccrual
int annualDays,
bool isAnnualized,
decimal resetCarryInterest = 0m,
decimal consumedInterest = 0m,
decimal closePercent = 1m)
decimal realizedInterest = 0m,
decimal unwindFraction = 1m)
{
decimal dynomicPrincipal = principal;
decimal interest = 0m;
decimal tdInterest = 0m;
decimal accrualBasis = notional;
decimal accrued = 0m;
for (int si = 0; si < segmentRates.Count; si++)
{
var (segStart, segRate) = segmentRates[si];
var segEnd = si < segmentRates.Count - 1
? segmentRates[si + 1].StartDate
: endDate;
// 重置日并本金
dynomicPrincipal = si == 0
? principal
: principal + interest;
accrualBasis = si == 0 ? notional : notional + accrued;
// 末日重置且 resetCarryInterest 非零:用存量替代
// 末日重置且 carry 非零:用存量替代
if (segEnd == endDate && si > 0 && resetCarryInterest != 0m)
dynomicPrincipal = principal + resetCarryInterest;
accrualBasis = notional + resetCarryInterest;
// 半开区间:重置日归下一段(旧代码逐日循环中重置日先更新本金再算息)
var segIncludeStart = (si == 0) ? boundary.IncludeStart : true;
var segIncludeEnd = (segEnd == endDate) ? boundary.IncludeEnd : false;
var days = SwapInterest.AccrualDays(segStart, segEnd, AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
var days = SwapInterest.AccrualDays(segmentRates[si].StartDate, segEnd,
AccrualBoundary.Of(segIncludeStart, segIncludeEnd));
if (days <= 0) continue;
var dailyRate = isAnnualized ? segRate / annualDays : segRate;
var daily = dynomicPrincipal * dailyRate;
var segInterest = daily * days;
interest += segInterest;
tdInterest += segInterest;
var dailyRate = isAnnualized ? segmentRates[si].Rate / annualDays : segmentRates[si].Rate;
accrued += accrualBasis * dailyRate * days;
}
// 扣除历史已结利息
interest -= consumedInterest * closePercent;
tdInterest -= consumedInterest * closePercent;
accrued -= realizedInterest * unwindFraction;
return new InterestResult(
SwapInterest.Round(interest, Precision),
SwapInterest.Round(tdInterest, Precision));
SwapInterest.Round(accrued, Precision),
SwapInterest.Round(accrued, Precision));
}
}
@@ -1601,11 +1601,11 @@ namespace YLErp.Modules.SwapModule
// 完整计息 trace:收集器由适配器创建,随后经 SwapCalcTrace 常驻落盘(关键路径日志,无条件)。
var interestTrace = new AccrualTrace();
var result = FundingLegAccrual.AccrueSimpleEod(
priorUnrealized: preEodPosition.InterestProfitSum,
priorAccrualPrincipal: preEodPosition.TdInterestPrincipal,
positionPrincipal: posiPrincipal,
closeRatio: closePercent,
originalPv: orginPv,
priorAccrued: preEodPosition.InterestProfitSum,
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
unwindFraction: closePercent,
referenceNotional: orginPv,
rate: legRate,
policy: accrualPolicy,
eodDate: endDate,