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"); } } }