diff --git a/UnitTestProject/Modules/SwapModule/Accrual/CompoundCarryInTest.cs b/UnitTestProject/Modules/SwapModule/Accrual/CompoundCarryInTest.cs
new file mode 100644
index 00000000..8c4af6da
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/Accrual/CompoundCarryInTest.cs
@@ -0,0 +1,107 @@
+using Newtonsoft.Json;
+using YLErp;
+using YLErp.Modules.SwapModule;
+using YLErp.Modules.SwapModule.Accrual;
+
+namespace UnitTestProject.Modules.SwapModule.Accrual
+{
+ ///
+ /// EQD-6977 carryInInterest 契约测试:
+ /// 1) 默认 0 与旧逐日循环逐位一致(加参零行为变化的安全证明);
+ /// 2) carry-in 仅在【首个重置日】并入计息基数(非窗口首日起息)——
+ /// 与"持有至到期"全期轨迹对齐的数学不变量:增量 = carryIn × 后续段日利率 × 后续段天数。
+ ///
+ [TestClass]
+ public class CompoundCarryInTest
+ {
+ private const decimal Notional = 100_000_000m;
+ private const decimal Spread = 0.0025m;
+ private const int AnnualDays = 365;
+ private static readonly DateTime StartDate = new(2026, 4, 21);
+ private static readonly DateTime EndDate = new(2026, 5, 11); // 21天 = 3×7,末日是重置日
+
+ private static swap_position CreatePosition()
+ {
+ return new swap_position
+ {
+ id = 1001, SwapTradeId = 1, PosiDirection = 0,
+ InterestDirection = (int)SwapDirectionEnum.收取,
+ InterestMode = (int)InterestModeEnum.标的期初全价,
+ InterestRateDefault = Spread,
+ InterestPrincipalFix = Notional,
+ PosiStartDate = StartDate, PosiMatuirityDate = StartDate.AddYears(1),
+ IsInitial = true, Invalid = false,
+ InterestType = (int)InterestTypeEnum.复利,
+ IsAnnualized = true, interest_rest_days = 7, interest_rule = 0,
+ FloatRateUnderlyingCode = null,
+ InterestSwapInterval = "[]"
+ };
+ }
+
+ private static List<(DateTime, decimal)> Segments()
+ => new()
+ {
+ (StartDate, Spread),
+ (StartDate.AddDays(7), Spread),
+ (StartDate.AddDays(14), Spread),
+ };
+
+ private sealed class StubSvc : SwapDealService
+ {
+ public StubSvc() : base(new OptUserInfo(0, nameof(CompoundCarryInTest), OptUserFrom.UnitTest)) { }
+ }
+
+ [TestMethod]
+ public void carryIn_默认省略_与旧逐日循环一致()
+ {
+ var position = CreatePosition();
+ var flowEvent = new swap_flow_event { InterestRate = Spread };
+
+ decimal oldI = 0, oldTd = 0;
+ new StubSvc().CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent,
+ AnnualDays, 0m, 1m, true, false, ref oldI, ref oldTd);
+
+ // 省略 carryInInterest(默认 0)
+ var r1 = CompoundInterestAccrual.AccruePeriod(
+ notional: Notional, segmentRates: Segments(),
+ startDate: StartDate, endDate: EndDate,
+ boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true,
+ resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
+ finalBasis: out _);
+ // 显式传 0 与省略等价
+ var r2 = CompoundInterestAccrual.AccruePeriod(
+ notional: Notional, segmentRates: Segments(),
+ startDate: StartDate, endDate: EndDate,
+ boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true,
+ resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
+ finalBasis: out _, trace: null, carryInInterest: 0m);
+
+ Assert.AreEqual((double)oldI, (double)r1.Accrued, 0.0000001, "省略 carryIn 与旧实现一致");
+ Assert.AreEqual((double)r1.Accrued, (double)r2.Accrued, 0.0000001, "省略与显式0一致");
+ }
+
+ [TestMethod]
+ public void carryIn_仅在首重置日起息_增量等于后续两段复利()
+ {
+ const decimal carryIn = 1_000_000m;
+
+ decimal Accrued(decimal c)
+ => CompoundInterestAccrual.AccruePeriod(
+ notional: Notional, segmentRates: Segments(),
+ startDate: StartDate, endDate: EndDate,
+ boundary: AccrualBoundary.Both, annualDays: AnnualDays, isAnnualized: true,
+ resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m,
+ finalBasis: out _, trace: null, carryInInterest: c).Accrued;
+
+ var delta = Accrued(carryIn) - Accrued(0m);
+
+ // Both 边界下三段各 7 天。carryIn 于 4/28(首个重置日)并入基数:
+ // 首段 [4/21,4/28] 不含 carryIn;其后两段 carryIn 自身起息且其首段利息再复利。
+ // 精确增量 = c×d + (c + c×d)×d = c×(2d + d²) = c×((1+d)² − 1),d = 7天利率因子。
+ var d = Spread * 7m / AnnualDays;
+ var expected = carryIn * (2m * d + d * d);
+ Assert.AreEqual((double)expected, (double)delta, 0.001,
+ "carryIn 增量 = 首个重置日起息的两段复利,首段不含 carryIn");
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
index eddbb07a..9a6b09fd 100644
--- a/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
+++ b/YLErpDAL/Modules/SwapModule/Accrual/CompoundInterestAccrual.cs
@@ -61,6 +61,11 @@ public static class CompoundInterestAccrual
/// 复利多日计息(替换 CalcDailyCompoundInterest 的纯数学部分)。
/// 从 startDate 到 endDate 全程重放,每个重置日把累计利息并入本金。
///
+ ///
+ /// 窗口前已计未结转利息(EQD-6977 罚息承接):在【首个重置日】并入计息基数——
+ /// 与"持有至到期"全期轨迹严格对齐(恒等式:全期复利 = 平仓日已结利息 + 罚息窗口利息)。
+ /// 默认 0 时与旧行为逐位一致。与 resetCarryInterest 互斥使用(后者是全平重放的末段存量替代)。
+ ///
public static InterestResult AccruePeriod(
decimal notional,
IReadOnlyList<(DateTime StartDate, decimal Rate)> segmentRates,
@@ -73,10 +78,14 @@ public static class CompoundInterestAccrual
decimal realizedInterest,
decimal unwindFraction,
out decimal finalBasis,
- AccrualTrace? trace = null)
+ AccrualTrace? trace = null,
+ decimal carryInInterest = 0m)
{
decimal accrualBasis = notional;
- decimal accrued = 0m;
+ // carryInInterest 是"窗口前已计未结转利息":先并入 accrued,随首个重置日的
+ // basis = notional + accrued 一并资本化,并在其后每个重置日持续留在基数里
+ //(与全窗口重放时 accrued 含全部历史利息的轨迹严格一致);最终报告时扣除。
+ decimal accrued = carryInInterest;
trace?.MarkStart(startDate, endDate, boundary, annualDays, isAnnualized);
@@ -116,6 +125,9 @@ public static class CompoundInterestAccrual
finalBasis = accrualBasis;
+ // 报告口径只含窗口内增量(carryIn 是窗口前已结利息,由正常平仓流单独结算)
+ accrued -= carryInInterest;
+
if (realizedInterest != 0m)
trace?.Unwind(endDate, unwindFraction, realizedInterest * unwindFraction, accrued - realizedInterest * unwindFraction);
accrued -= realizedInterest * unwindFraction;