using System; using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using YLErp; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule; using YLErp.Modules.SwapModule.Accrual; using YLErp.Derivatives.Interest; namespace UnitTestProject.Modules.SwapModule.Accrual { /// /// 影子测试:CalcDailySimpleInterest(旧逐日循环)vs SimpleInterestAccrual.AccruePeriod(新分段纯函数)。 /// [TestClass] public class SimplePeriodShadowTest { 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天, 7天周期→重置日 4/28, 5/5 private static trade CreateTrade() { return new trade { id = 1, TradeNumber = "UT-SIMPLE-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(SimplePeriodShadowTest), OptUserFrom.UnitTest)) { } } /// 带浮动率 stub 的 SwapDealService:override IndexFixer 注入预设 FR007 取价。 private sealed class FloatStubSvc : SwapDealService { private readonly IIndexFixer _fixer; public FloatStubSvc(IIndexFixer fixer) : base(new OptUserInfo(0, nameof(SimplePeriodShadowTest), OptUserFrom.UnitTest)) => _fixer = fixer; protected override IIndexFixer IndexFixer => _fixer; } /// Stub IIndexFixer:对所有查询返回固定 fixing(不依赖日期匹配,规避 QDP 日历差异)。 private sealed class StubIndexFixer : IIndexFixer { private readonly decimal _rate; public StubIndexFixer(decimal rate) => _rate = rate; public bool TryGetFixing(DateTime fixingDate, string underlyingCode, out decimal rate) { rate = _rate; return true; } } /// /// 固定利率(无FR007)算头不算尾,全平,无历史归档。 /// [TestMethod] public void 影子_固定利率_无归档_旧新一致() { var td = CreateTrade(); var position = CreatePosition(); var flowEvent = new swap_flow_event { InterestRate = Spread }; var preEod = new eod_swap_position { id = 0, TdInterestPrincipal = 0, InterestProfitSum = 0 }; // 旧方法 decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, AnnualDays, 0m, 1m, Notional, true, false, ref oldI, ref oldTd); // 新方法:固定利率全段相同 // 旧代码差分: dynomicPrincipal = preEod.TdInterestPrincipal(=0) + posiPrincipal - orginPv = 0 var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; var accrualPrincipal = 0m + Notional - Notional; // 差分 = 0 var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 0m, notional: 0m, unwindFraction: 1m, segmentRates: segRates, startDate: StartDate, endDate: EndDate, priorValueDate: DateTime.MinValue, boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true); 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 一致"); } /// /// 有历史归档(preEod.id != 0),续接上一日终。 /// [TestMethod] public void 影子_有归档_续接_旧新一致() { var position = CreatePosition(); var preEodDate = new DateTime(2026, 5, 4); // 上一日终 = 第14天 var preEod = new eod_swap_position { id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = preEodDate, TdInterestPrincipal = Notional, InterestProfitSum = 200_000m, PosiNotionalValue = Notional, FloatRate = 0m }; var flowEvent = new swap_flow_event { InterestRate = Spread }; // 旧方法 decimal oldI = 0, oldTd = 0; var svc = new StubSvc(); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, AnnualDays, 0m, 0.5m, Notional, true, false, ref oldI, ref oldTd); // 新方法 // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv var accrualPrincipal = Notional + Notional - Notional; var segRates = new List<(DateTime, decimal)> { (StartDate, Spread) }; var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 200_000m * 0.5m, notional: accrualPrincipal, unwindFraction: 0.5m, segmentRates: segRates, startDate: StartDate, endDate: EndDate, priorValueDate: preEodDate, boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true); 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 一致"); } /// /// 单利 + FR007 浮动利率 + 多重置日 + 部分平仓 + 有历史归档。 /// 验证旧方法内部取价循环生成的 segmentRates 与手算一致——为抽取共享 SegmentRateBuilder 做安全网。 /// 场景:preEod.ValueDate=4/30,重置日 4/21(跳过取价), 4/28(跳过取价), 5/5(取 FR007 fixing)。 /// [TestMethod] public void 影子_单利浮动_FR007_部分平仓_旧新一致() { const decimal floatRateIn = 0.0150m; // 入参 floateRate(上一次取到的浮动率 1.50%) const decimal fixingAtReset = 0.0125m; // 5/5 重置日取到的 FR007 fixing(1.25%) const decimal closePct = 0.5m; var preEodDate = new DateTime(2026, 4, 30); // 上一日终=4/30,5/5 > 4/30 触发取价 var position = CreatePosition(); position.FloatRateUnderlyingCode = "FR007"; var preEod = new eod_swap_position { id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = preEodDate, TdInterestPrincipal = Notional, InterestProfitSum = 200_000m, PosiNotionalValue = Notional, FloatRate = 0m }; var flowEvent = new swap_flow_event { InterestRate = Spread }; // 旧方法(通过 stub IndexFixer 注入 FR007 取价) decimal oldI = 0, oldTd = 0; var svc = new FloatStubSvc(new StubIndexFixer(fixingAtReset)); svc.CalcDailySimpleInterest(preEod, EndDate, position, Notional, flowEvent, AnnualDays, floatRateIn, closePct, Notional, true, false, ref oldI, ref oldTd); // 新方法:手算 segmentRates(对齐旧代码取价循环的逻辑) // 4/21 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn // 4/28 <= preEodDate(4/30) → 跳过取价,currentFloat 保持入参 floatRateIn // 5/5 > preEodDate(4/30) → 取价,currentFloat 更新为 fixingAtReset var segRates = new List<(DateTime, decimal)> { (StartDate, Spread + floatRateIn), // (4/21, 0.0175) (StartDate.AddDays(7), Spread + floatRateIn), // (4/28, 0.0175) (StartDate.AddDays(14), Spread + fixingAtReset), // (5/5, 0.0150) }; // 差分本金 = preEod.TdInterestPrincipal + posiPrincipal - orginPv var accrualPrincipal = Notional + Notional - Notional; var result = SimpleInterestAccrual.AccruePeriod( priorAccrued: 200_000m * closePct, notional: accrualPrincipal, unwindFraction: closePct, segmentRates: segRates, startDate: StartDate, endDate: EndDate, priorValueDate: preEodDate, boundary: AccrualBoundary.StartOnly, annualDays: AnnualDays, isAnnualized: true); 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 一致"); } } }