Files
zszq-trs/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs
T
hjhan 2f7caa7a1f 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失败(基线一致)。
2026-08-12 10:20:13 +08:00

145 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.
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 = endDate1天),无重置日。
/// 期望利息 = 本金 × 日利率 = 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);
}
}
}