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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user