using Newtonsoft.Json; using YLErp; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Accrual; namespace UnitTestProject.Modules.SwapModule.Accrual { /// /// 影子测试:CalcDailyCompoundInterest(旧逐日循环)vs CompoundInterestAccrual.AccruePeriod(新分段纯函数)。 /// [TestClass] public class CompoundPeriodShadowTest { 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 trade CreateTrade() { return new trade { id = 1, TradeNumber = "UT-COMPOUND-SHADOW", ClientId = 999998, TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, ExerciseDate = StartDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", SettlementRules = 0 }) } }; } 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 sealed class StubSvc : SwapDealService { public StubSvc() : base(new OptUserInfo(0, nameof(CompoundPeriodShadowTest), OptUserFrom.UnitTest)) { } } /// /// 固定利率(无FR007)算头不算尾,全平。 /// 重置日 4/28, 5/5, 末日 5/11 恰为重置日(21天=3×7)。 /// [TestMethod] public void 影子_固定利率_全平_末日重置日_旧新一致() { var position = CreatePosition(); var flowEvent = new swap_flow_event { InterestRate = Spread }; var preEod = new eod_swap_position { id = 0 }; // 旧方法 decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, AnnualDays, 0m, 1m, true, false, ref oldI, ref oldTd); // 新方法:固定利率全段相同,分段点 = PosiStartDate + k×7 // 段: [4/21,4/28), [4/28,5/5), [5/5,5/11] → 注意旧代码 calcLast=false 不算末日 var allInRate = Spread; // 无浮动利率 var segRates = new List<(DateTime, decimal)> { (StartDate, allInRate), (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; var result = CompoundInterestAccrual.AccruePeriod( notional: Notional, segmentRates: segRates, startDate: StartDate, endDate: EndDate, boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true, resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m, finalBasis: out _); Console.WriteLine($"旧: I={oldI} Td={oldTd}"); Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}"); Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致"); Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致"); } /// /// 部分平仓 30% + consumedInterest 扣除。 /// [TestMethod] public void 影子_部分平仓30pct_consumedInterest_旧新一致() { var position = CreatePosition(); var flowEvent = new swap_flow_event { InterestRate = Spread }; var preEod = new eod_swap_position { id = 0 }; const decimal consumed = 50_000m; const decimal closePct = 0.3m; const decimal carry = 0m; // 无历史归档 // 旧方法 decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional * closePct, flowEvent, AnnualDays, 0m, closePct, true, false, ref oldI, ref oldTd, consumedInterest: consumed, resetCarryInterest: carry); // 新方法 var allInRate = Spread; var segRates = new List<(DateTime, decimal)> { (StartDate, allInRate), (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; var result = CompoundInterestAccrual.AccruePeriod( notional: Notional * closePct, segmentRates: segRates, startDate: StartDate, endDate: EndDate, boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true, resetCarryInterest: carry, realizedInterest: consumed, unwindFraction: closePct, finalBasis: out _); Console.WriteLine($"旧: I={oldI} Td={oldTd}"); Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}"); Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致"); Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致"); } /// /// 算头算尾(calcMode="11")对比。 /// [TestMethod] public void 影子_算头算尾_旧新一致() { var position = CreatePosition(); var flowEvent = new swap_flow_event { InterestRate = Spread }; var preEod = new eod_swap_position { id = 0 }; // 旧方法 decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailyCompoundInterest(EndDate, position, Notional, flowEvent, AnnualDays, 0m, 1m, true, true, ref oldI, ref oldTd); // 新方法 var allInRate = Spread; var segRates = new List<(DateTime, decimal)> { (StartDate, allInRate), (StartDate.AddDays(7), allInRate), (StartDate.AddDays(14), allInRate), }; var result = CompoundInterestAccrual.AccruePeriod( notional: Notional, segmentRates: segRates, startDate: StartDate, endDate: EndDate, boundary: AccrualBoundary.Both, annualDays: AnnualDays, isAnnualized: true, resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m, finalBasis: out _); Console.WriteLine($"旧: I={oldI} Td={oldTd}"); Console.WriteLine($"新: Accrued={result.Accrued} AccruedToday={result.AccruedToday}"); Assert.AreEqual((double)oldI, (double)result.Accrued, 0.01, "InterestAmount 一致"); Assert.AreEqual((double)oldTd, (double)result.AccruedToday, 0.01, "TdInterestAmount 一致"); } } }