using Newtonsoft.Json; using YLErp.DBModels; using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// /// GLMS-20260703-0002 债券TRS 平仓利息差异 - 仅调用系统函数复现 /// ============================================================================ /// 客户测试环境这笔债券TRS(截图确认): /// 成交/持仓名义本金 = 279,486,108.21 /// 起息日(StartDate=SettleDate) = 2026-07-06,平仓日(UnwindDate) = 2026-07-20 /// 年化天数 = 365,计息模式 = 算头不算尾("10"),计息天数 = 14 天 /// 利率 = FR007-1.55%(浮动利率腿),重置频率 = 7 天,interest_rule = 0(当前营业日) /// 截图 FR007:07-06=1.42%, 07-13=1.425%(重置点取当前营业日) /// /// 观察结果: /// 系统计算的平仓盈亏(利息端)= -13,667.85 /// 实际应得平仓盈亏(利息端)= -13,668.02 /// 差异 = 0.17 元 /// /// 代码走查结论(SwapDealService): /// 平仓路径:GetInterests → CalcSwapDealInterest → CalcUnwindInterest /// → InitSwapDealInterest → CalcDailyCompoundInterest / CalcDailySimpleInterest /// FR007 取数规则:只在"重置日"(i % interest_rest_days == 0) 取一次 FR007,非重置日沿用上一重置日。 /// 本例 07-06~07-20 跨 2 个重置周期,取 2 次 FR007: /// - 第一段(07-06~07-12):取 07-06 当前营业日 FR007 = 1.42% /// - 第二段(07-13~07-19):取 07-13 当前营业日 FR007 = 1.425% /// 差异根因:系统配置走 <复利>,导致 07-13 重置日把前 7 天累计利息并入本金, /// 第二段计息本金降为 279,479,140.20,最终利息绝对值比单利少 0.17 元。 /// 若按业务口径走 <单利>,则本金全程保持 279,486,108.21,利息 = -13,668.02。 /// /// 本测试不再做任何手工计算(手算/验算见同目录 Excel:GLMS20260703_平仓利息验算.xlsx), /// 只做:构造输入 → 调用系统真实函数 GetInterests → 与截图已知结果断言。 /// ============================================================================ [TestClass] public class GLMS20260703CloseInterestTest { #region 内部类:固定利率模拟服务(不触碰 DB,但按日期返回 FR007 截图值) private sealed class StubSwapDealService : SwapDealService { public StubSwapDealService(OptUserInfo optUser) : base(optUser) { } /// 单元测试记录:生产代码每次调用 TryGetFloatRate 的日期与结果。 public readonly List<(DateTime RequestDate, double Rate)> FloatRateCalls = new(); /// /// 按截图 eod_commodity_future_price.ValueDate 返回 FR007 ReferencePrice(小数)。 /// 生产环境由 TryGetFloatRate 去行情/DB 取数;单元测试用截图硬编码快照替代。 /// protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { rate = 0d; if (underlyingCode != "FR007") return false; var fr007 = new Dictionary { [new DateTime(2026, 7, 3)] = 0.0143, [new DateTime(2026, 7, 6)] = 0.0142, [new DateTime(2026, 7, 7)] = 0.0143, [new DateTime(2026, 7, 8)] = 0.0143, [new DateTime(2026, 7, 9)] = 0.0143, [new DateTime(2026, 7, 10)] = 0.0142, [new DateTime(2026, 7, 13)] = 0.01425, [new DateTime(2026, 7, 14)] = 0.0143, [new DateTime(2026, 7, 15)] = 0.0144, [new DateTime(2026, 7, 16)] = 0.0144, [new DateTime(2026, 7, 17)] = 0.0144, [new DateTime(2026, 7, 20)] = 0.0143, }; if (fr007.TryGetValue(valueDate.Date, out rate)) { FloatRateCalls.Add((valueDate.Date, rate)); return true; } // 若请求日期不在硬编码表(如 interest_rule=-1 调到周末),返回最近有值日的 FR007 var nearest = fr007.Keys.OrderByDescending(d => d) .FirstOrDefault(d => d <= valueDate.Date); if (nearest != default) { rate = fr007[nearest]; FloatRateCalls.Add((valueDate.Date, rate)); return true; } return false; } } #endregion #region 测试常量(来自客户测试环境截图 GLMS-20260703-0002) /// 成交/持仓名义本金 private const decimal Notional = 279486108.21m; /// 年化天数 private const int AnnualDays = 365; /// 固定利差 -1.55%(FR007-1.55%) private const decimal Spread = -0.0155m; /// 起息日(td.StartDate = SettleDate,不是成交日) private static readonly DateTime StartDate = new(2026, 7, 6); /// 成交日(td.TradeDate,仅作对照;不参与计息起点) private static readonly DateTime TradeDate = new(2026, 7, 3); /// 平仓日(valueDate / unwindDate) private static readonly DateTime CloseDate = new(2026, 7, 20); /// 系统实际计算的利息绝对值 private const decimal SystemInterestAmount = 13667.85m; /// 实际应得利息绝对值 private const decimal ActualInterestAmount = 13668.02m; #endregion private SwapDealService _service; [TestInitialize] public void Init() { _service = new StubSwapDealService( new OptUserInfo(0, nameof(GLMS20260703CloseInterestTest), OptUserFrom.UnitTest)); } #region 测试数据构建器(仅构造输入,不计算利息) private static trade CreateTrade() { var extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, InterestCalcMode = "10", // 算头不算尾 SettlementRules = 0 }) }; return new trade { id = 1, TradeNumber = "GLMS-20260703-0002", ClientId = 999998, TradeType = "债券TRS", TradeDate = TradeDate, StartDate = StartDate, ExerciseDate = CloseDate.AddDays(1), // 必须 > valueDate,否则 InitInterestDate 会多减一天 TradeStatus = "已平仓", ValidState = "Valid", trade_extend = extend }; } /// /// 构造债券TRS浮动利率本金腿。FloatRateUnderlyingCode=FR007,利率=FR007-1.55%。 /// 仅设置输入字段;利息由系统函数 CalcDailyCompoundInterest / CalcDailySimpleInterest 计算。 /// private static swap_position CreateBondPosition(InterestTypeEnum interestType, int interestRule = 0) { var intervalModels = new List { new IntervalModel { Date = CloseDate, Rate = Spread, Settlement = 0 } }; return new swap_position { id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, // 债券本金腿:标的期初全价(closePrincipal = posiNotional × closePercent) InterestDirection = (int)SwapDirectionEnum.支付, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Spread, InterestPrincipalFix = Notional, PosiStartDate = StartDate, PosiMatuirityDate = CloseDate, IsInitial = true, Invalid = false, InterestType = (int)interestType, // 复利 or 单利 IsAnnualized = true, interest_rest_days = 7, // 7天重置 interest_rule = interestRule, // 0=当前营业日, -1=前一营业日 FloatRateUnderlyingCode = "FR007", // 真实浮动利率腿 FloatRate = 0m, PosiNotionalValue = Notional, UnderlyingCode = "2500002.IB", InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) }; } /// /// 驱动真实平仓利息路径(settment=false → CalcUnwindInterest → InitSwapDealInterest /// → CalcDailyCompoundInterest / CalcDailySimpleInterest)。 /// eodPositions 传空 → 等效"无前日日终快照",日循环从起始日重算。 /// private swap_flow_event CalcCloseInterest(InterestTypeEnum interestType, int interestRule = 0) { var td = CreateTrade(); var position = CreateBondPosition(interestType, interestRule); // 每次计算前清空取数记录,避免同一测试中多次调用互相污染 ((StubSwapDealService)_service).FloatRateCalls.Clear(); var interests = _service.GetInterests( td, td.trade_extend, CloseDate, CloseDate, // valueDate / unwindDate new List(), // eodPositions(空) new List { position }, Notional, Notional, Notional, Notional, // posiNotional / long / short / closePosiNotional 1m, // closePercent (int)SwapEventTypeEnum.平仓, false, false, 0m, Notional, // tdClose / needPrice / grossPrice / orginPv false, settment: false, newCalcLast: false, closeList: null); Assert.AreEqual(1, interests.Count); return interests[0]; } #endregion #region 复现用例(仅调用系统函数 + 对照已知结果断言) /// /// [GLMS20260703_REPRO_001] 系统路径:InterestType=复利 + 真实FR007 → 复现系统值 -13,667.85 /// 与生产代码完全一致:7天重置,重置日取当前营业日 FR007,利息并入本金。 /// [TestMethod] public void Reproduce_SystemValue_13667_85_WithCompoundInterest() { var interest = CalcCloseInterest(InterestTypeEnum.复利); var amount2 = Math.Round(interest.InterestAmount, 2, MidpointRounding.AwayFromZero); var closePnl2 = Math.Round(interest.InterestClosePnL, 2, MidpointRounding.AwayFromZero); Console.WriteLine("系统路径(复利):"); Console.WriteLine($" 利息金额(2位显示)={amount2}"); Console.WriteLine($" 平仓盈亏(利息端,2位)={closePnl2}"); // 截图给的是"平仓盈亏(利息端)"的绝对值口径,故比较绝对值。 Assert.AreEqual(SystemInterestAmount, Math.Abs(amount2), $"系统复利路径应得到利息绝对值 {SystemInterestAmount},实际 {amount2}"); Assert.AreEqual(SystemInterestAmount, Math.Abs(closePnl2), $"支付方向平仓盈亏绝对值应为 {SystemInterestAmount},实际 {closePnl2}"); } /// /// [GLMS20260703_REPRO_002] 实际口径:InterestType=单利 + 真实FR007 → 复现实际值 -13,668.02 /// 同一 position、同一 FR007、同一计息天数,仅把 InterestType 改为单利, /// 利息不并入本金,全程用名义本金 279,486,108.21 计息。 /// [TestMethod] public void Reproduce_ActualValue_13668_02_WithSimpleInterest() { var interest = CalcCloseInterest(InterestTypeEnum.单利); var amount2 = Math.Round(interest.InterestAmount, 2, MidpointRounding.AwayFromZero); var closePnl2 = Math.Round(interest.InterestClosePnL, 2, MidpointRounding.AwayFromZero); Console.WriteLine("实际口径(单利):"); Console.WriteLine($" 利息金额(2位显示)={amount2}"); Console.WriteLine($" 平仓盈亏(利息端,2位)={closePnl2}"); Assert.AreEqual(ActualInterestAmount, Math.Abs(amount2), $"单利路径应得到利息绝对值 {ActualInterestAmount},实际 {amount2}"); Assert.AreEqual(ActualInterestAmount, Math.Abs(closePnl2), $"支付方向平仓盈亏绝对值应为 {ActualInterestAmount},实际 {closePnl2}"); } /// /// [GLMS20260703_REPRO_003] 差异定位:0.17 元 = 复利 vs 单利 /// 同一笔交易、同一 FR007 取值、同一计息天数,唯一区别是 InterestType, /// 系统(复利)比实际(单利)少 0.17 元。 /// [TestMethod] public void PrecisionGap_Is_0_17_ComplexVsSimple() { var compound = CalcCloseInterest(InterestTypeEnum.复利).InterestAmount; var simple = CalcCloseInterest(InterestTypeEnum.单利).InterestAmount; // 两者都是负数(支付方向),取绝对值差异 var gap = Math.Round(Math.Abs(simple) - Math.Abs(compound), 2, MidpointRounding.AwayFromZero); Console.WriteLine($"复利 |利息|={Math.Abs(compound):F11}"); Console.WriteLine($"单利 |利息|={Math.Abs(simple):F11}"); Console.WriteLine($"差异(2位)={gap}"); Assert.AreEqual(0.17m, gap, "单利与复利的利息绝对值差异应为 0.17 元"); } /// /// [GLMS20260703_REPRO_004] 验证真实函数确实在 07-06、07-13 两个重置点取了 FR007 /// (不手算利息,仅检查系统函数实际发起了哪几次取数)。 /// [TestMethod] public void Trace_FloatRate_Taken_Dates() { var fe = CalcCloseInterest(InterestTypeEnum.复利, interestRule: 0); var calls = ((StubSwapDealService)_service).FloatRateCalls; Console.WriteLine("TryGetFloatRate 实际调用记录(按调用顺序):"); foreach (var (date, rate) in calls) { Console.WriteLine($" 请求日期={date:yyyy-MM-dd} 返回 FR007={rate:P4}"); } Assert.IsTrue(calls.Any(c => c.RequestDate == new DateTime(2026, 7, 6)), "应取 07-06 的 FR007"); Assert.IsTrue(calls.Any(c => c.RequestDate == new DateTime(2026, 7, 13)), "应取 07-13 的 FR007"); Assert.AreEqual(SystemInterestAmount, Math.Abs(Math.Round(fe.InterestAmount, 2, MidpointRounding.AwayFromZero)), "interest_rule=0 复利应复现系统值 -13,667.85"); } #endregion } }