diff --git a/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs b/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs
new file mode 100644
index 00000000..816be349
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Accrual/SwapInterest_CompoundInArrears_RolloverTimingTests.cs
@@ -0,0 +1,144 @@
+using System.Text.RegularExpressions;
+using YLErp.Core.Interest;
+using YLErp.Derivatives.Interest;
+
+namespace UnitTestProject.Modules.SwapModule.Accrual
+{
+ ///
+ /// 聚焦测试:AccrueCompoundInArrears 的「本金滚存时机」必须符合确认书规定。
+ /// 核心不变量:本金只允许在重置日/段末滚入利息,非重置日不得资本化。
+ ///
+ /// 与原草稿的关键区别:本版直接通过 AccrualTrace 断言不变量。
+ /// 真实实现在每次段末会发出 ROLLOVER 事件并记录 newBasis(见 SwapInterest.cs:215 /
+ /// AccrualTrace.Rollover),因此「非重置日是否发生资本化」是可程序化验证的,
+ /// 无需仅靠总利息回归来保护(原草稿的自我怀疑"无法断言计息基数"已不成立)。
+ ///
+ [TestClass]
+ public class SwapInterest_CompoundInArrears_RolloverTimingTests
+ {
+ private const int FundingLegPrecision = 12;
+ private const int AnnualDays = 365;
+
+ ///
+ /// 场景: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段计息基数恒为原始本金、段内未提前资本化。
+ ///
+ [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 { 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,
+ "重置日滚入的本金应为原始本金 + 前段利息,证明段内未提前资本化");
+ }
+
+ ///
+ /// 极端场景:startDate = endDate(1天),无重置日。
+ /// 期望利息 = 本金 × 日利率 = 1,000,000 × 0.0365/365 = 100。
+ /// 且唯一 ROLLOVER 必须落在窗口终点(=startDate),无任何内部重置滚存。
+ ///
+ [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());
+ }
+
+ ///
+ /// 段内无重置日:验证整段等同于单利,且不发生任何内部滚存。
+ /// 6天窗口(01-01..01-06)在7天重置周期内,Both 边界含两端 = 6 个计息日,
+ /// 期望利息 = 本金 × 日利率 × 6 = 600。
+ ///
+ [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);
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
index d291a37b..b2f4dab1 100644
--- a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
+++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs
@@ -7,12 +7,12 @@ namespace YLErp.Modules.SwapModule.Accrual;
/// 融资腿计息编排层——纯数学部分(替换 SwapDealService 内 CalcDaily* 家族的纯计算)。
///
/// 命名规范(对齐 QuantLib / Strata):
-/// - notional/principal → 计息基数
+/// - notional → 计息名义本金(不用 principal,swap leg 用 notional 是业界标准)
/// - accrued → 累计应计利息
/// - unwindFraction → 平仓比例(0~1)
/// - realizedInterest → 历史已结利息(legacy: consumedInterest)
-/// - referenceNotional → 差分公式参考本金(legacy: orginPv/originalPv)
-/// - accrualBasis → 逐段滚动的计息基数(legacy: dynomicPrincipal)
+/// - baseNotional → 差分公式基数(legacy: orginPv/originalPv)
+/// - accrualBasis → 逐段滚动的计息基数(legacy: dynomicPrincipal=dynamicPrincipal typo)
///
public static class FundingLegAccrual
{
@@ -21,23 +21,18 @@ public static class FundingLegAccrual
///
/// 单利日终计息(替换 CalcDailySimpleInterestByEod 的纯数学部分)。
///
- /// 上一日终累计应计利息。
- /// 上一日终计息基数。
- /// 当前持仓名义本金。
- /// 平仓比例(EOD 恒为 1)。
- /// 差分公式参考本金(legacy: orginPv)。
public static InterestResult AccrueSimpleEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
- decimal referenceNotional,
+ decimal baseNotional,
FundingLegRate rate,
AccrualPolicy policy,
DateTime eodDate,
AccrualTrace? trace = null)
{
- var basis = priorNotional + notional - referenceNotional;
+ var basis = priorNotional + notional - baseNotional;
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
@@ -62,14 +57,14 @@ public static class FundingLegAccrual
///
/// 复利日终计息(替换 CalcDailyCompoundInterestByEod 的纯数学部分)。
/// 重置日:basis = notional + priorAccrued × remainingFraction。
- /// 非重置日:basis = priorNotional + notional - referenceNotional。
+ /// 非重置日:basis = priorNotional + notional - baseNotional。
///
public static InterestResult AccrueCompoundEod(
decimal priorAccrued,
decimal priorNotional,
decimal notional,
decimal unwindFraction,
- decimal referenceNotional,
+ decimal baseNotional,
FundingLegRate rate,
AccrualPolicy policy,
bool isResetDay,
@@ -79,7 +74,7 @@ public static class FundingLegAccrual
{
var basis = isResetDay
? notional + priorAccrued * remainingFraction
- : priorNotional + notional - referenceNotional;
+ : priorNotional + notional - baseNotional;
var displayBasis = basis * unwindFraction;
var allInRate = rate.AllInRate;
@@ -105,11 +100,6 @@ public static class FundingLegAccrual
/// 单利多日计息(替换 CalcDailySimpleInterest 的纯数学部分)。
/// 本金全程恒定,按重置日分段取利率。
///
- /// 上一日终累计应计利息。
- /// 计息基数(差分,全程恒定)。
- /// 平仓比例。
- /// 分段利率表:(段起日, all-in利率),按日期升序。
- /// 上一日终归档日(只算此日之后的利息)。
public static InterestResult AccrueSimplePeriod(
decimal priorAccrued,
decimal notional,
@@ -157,10 +147,6 @@ public static class FundingLegAccrual
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
///
- /// 本次平仓名义本金(已按 unwindFraction 缩放)。
- /// 末日重置时的存量利息(非末日传 0)。
- /// 历史已结利息(legacy: consumedInterest)。
- /// 平仓比例。
public static InterestResult AccrueCompoundPeriod(
decimal notional,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index d4696526..48ff75c2 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -1605,7 +1605,7 @@ namespace YLErp.Modules.SwapModule
priorNotional: preEodPosition.TdInterestPrincipal,
notional: posiPrincipal,
unwindFraction: closePercent,
- referenceNotional: orginPv,
+ baseNotional: orginPv,
rate: legRate,
policy: accrualPolicy,
eodDate: endDate,