refactor(accrual): referenceNotional→baseNotional, notional统一
- referenceNotional → baseNotional(更短更清晰) - 新代码统一用 notional(不用 principal): QuantLib/Strata 对 swap leg 用 notional 是业界标准 - DB字段(InterestPrincipalFix等)保持不动(legacy) - 注释标注 legacy 对照(orginPv/originalPv/dynomicPrincipal) 验证: 编译0错误, 7影子测试全过, 全量523测试7失败(基线一致)。
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using YLErp.Core.Interest;
|
||||
using YLErp.Derivatives.Interest;
|
||||
|
||||
namespace UnitTestProject.Modules.SwapModule.Accrual
|
||||
{
|
||||
/// <summary>
|
||||
/// 聚焦测试:AccrueCompoundInArrears 的「本金滚存时机」必须符合确认书规定。
|
||||
/// 核心不变量:本金只允许在重置日/段末滚入利息,非重置日不得资本化。
|
||||
///
|
||||
/// 与原草稿的关键区别:本版<b>直接通过 AccrualTrace 断言不变量</b>。
|
||||
/// 真实实现在每次段末会发出 ROLLOVER 事件并记录 newBasis(见 SwapInterest.cs:215 /
|
||||
/// AccrualTrace.Rollover),因此「非重置日是否发生资本化」是可程序化验证的,
|
||||
/// 无需仅靠总利息回归来保护(原草稿的自我怀疑"无法断言计息基数"已不成立)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapInterest_CompoundInArrears_RolloverTimingTests
|
||||
{
|
||||
private const int FundingLegPrecision = 12;
|
||||
private const int AnnualDays = 365;
|
||||
|
||||
/// <summary>
|
||||
/// 场景:14天窗口,第8天(01-08)重置一次,利率恒定 3.65%(日利率 0.01%)。
|
||||
/// 验证:
|
||||
/// (1) 总利息 = 1400.49(第1期700 + 第2期700.49);
|
||||
/// (2) ROLLOVER 仅发生在重置日(01-08)与窗口终点(01-15),非重置日(如01-03)绝不滚存;
|
||||
/// (3) 重置日 ROLLOVER 的 newBasis = 原始本金 + 前7天利息 = 1,000,700,
|
||||
/// 证明第1段计息基数恒为原始本金、段内未提前资本化。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void InterestPrincipal_ShouldRollOnlyOnResetDays_NotOnNonResetDays()
|
||||
{
|
||||
var startDate = new DateTime(2026, 1, 1);
|
||||
var endDate = new DateTime(2026, 1, 15);
|
||||
|
||||
var principal = 1_000_000m;
|
||||
var rate = 0.0365m;
|
||||
var resetDates = new List<DateTime> { new DateTime(2026, 1, 8) };
|
||||
var trace = new AccrualTrace();
|
||||
var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace);
|
||||
|
||||
var result = SwapInterest.AccrueCompoundInArrears(
|
||||
ctx,
|
||||
principal,
|
||||
rate,
|
||||
startDate,
|
||||
endDate,
|
||||
AccrualBoundary.Both,
|
||||
resetDates);
|
||||
|
||||
Assert.AreEqual(1400.49m, Math.Round(result.Accrued, 2));
|
||||
|
||||
var rolloverDates = trace.Entries
|
||||
.Where(e => e.Step == AccrualTraceEvent.Rollover)
|
||||
.Select(e => e.Date)
|
||||
.ToList();
|
||||
|
||||
var allowed = resetDates.Concat(new[] { endDate }).OrderBy(d => d).ToList();
|
||||
CollectionAssert.AreEqual(allowed, rolloverDates.OrderBy(d => d).ToList());
|
||||
|
||||
Assert.IsFalse(rolloverDates.Contains(new DateTime(2026, 1, 3)),
|
||||
"非重置日发生了本金滚存,违反确认书规定");
|
||||
|
||||
var resetRollover = trace.Entries
|
||||
.First(e => e.Step == AccrualTraceEvent.Rollover && e.Date == new DateTime(2026, 1, 8));
|
||||
var newBasis = ParseNewBasis(resetRollover.Line);
|
||||
Assert.AreEqual(principal + 700m, newBasis,
|
||||
"重置日滚入的本金应为原始本金 + 前段利息,证明段内未提前资本化");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 极端场景:startDate = endDate(1天),无重置日。
|
||||
/// 期望利息 = 本金 × 日利率 = 1,000,000 × 0.0365/365 = 100。
|
||||
/// 且唯一 ROLLOVER 必须落在窗口终点(=startDate),无任何内部重置滚存。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SingleDay_ShouldNotRollInterest_NoResetDay()
|
||||
{
|
||||
var date = new DateTime(2026, 1, 1);
|
||||
var principal = 1_000_000m;
|
||||
var rate = 0.0365m;
|
||||
var trace = new AccrualTrace();
|
||||
var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace);
|
||||
|
||||
var result = SwapInterest.AccrueCompoundInArrears(
|
||||
ctx,
|
||||
principal,
|
||||
rate,
|
||||
date,
|
||||
date,
|
||||
AccrualBoundary.Both);
|
||||
|
||||
Assert.AreEqual(100m, Math.Round(result.Accrued, 2));
|
||||
|
||||
var rolloverDates = trace.Entries
|
||||
.Where(e => e.Step == AccrualTraceEvent.Rollover)
|
||||
.Select(e => e.Date)
|
||||
.ToList();
|
||||
CollectionAssert.AreEqual(new[] { date }, rolloverDates.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 段内无重置日:验证整段等同于单利,且不发生任何内部滚存。
|
||||
/// 6天窗口(01-01..01-06)在7天重置周期内,Both 边界含两端 = 6 个计息日,
|
||||
/// 期望利息 = 本金 × 日利率 × 6 = 600。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void WithinPeriod_NoRollover_ShouldMatchSimpleInterest()
|
||||
{
|
||||
var startDate = new DateTime(2026, 1, 1);
|
||||
var endDate = new DateTime(2026, 1, 6);
|
||||
var principal = 1_000_000m;
|
||||
var rate = 0.0365m;
|
||||
var trace = new AccrualTrace();
|
||||
var ctx = new AccrualContext(AnnualDays, FundingLegPrecision, trace);
|
||||
|
||||
var result = SwapInterest.AccrueCompoundInArrears(
|
||||
ctx,
|
||||
principal,
|
||||
rate,
|
||||
startDate,
|
||||
endDate,
|
||||
AccrualBoundary.Both);
|
||||
|
||||
// 计息天数必须用边界感知的 AccrualDays,不能拿 (end-start).Days(会少算1天)
|
||||
var days = SwapInterest.AccrualDays(startDate, endDate, AccrualBoundary.Both); // = 6
|
||||
var expected = Math.Round(principal * rate * days / AnnualDays, FundingLegPrecision, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(expected, Math.Round(result.Accrued, 10));
|
||||
|
||||
var rolloverDates = trace.Entries
|
||||
.Where(e => e.Step == AccrualTraceEvent.Rollover)
|
||||
.Select(e => e.Date)
|
||||
.ToList();
|
||||
CollectionAssert.AreEqual(new[] { endDate }, rolloverDates.ToArray());
|
||||
}
|
||||
|
||||
private static decimal ParseNewBasis(string line)
|
||||
{
|
||||
var m = Regex.Match(line, @"newBasis=([0-9.]+)");
|
||||
Assert.IsTrue(m.Success, $"ROLLOVER 行缺少 newBasis:{line}");
|
||||
return decimal.Parse(m.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user