From 98bfe8fb5dbce7afaeb8b5a57edfb4e433ed9067 Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 20 Jul 2026 18:31:19 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20=E5=A2=9E=E5=8A=A0=20GLMS-2026070?= =?UTF-8?q?3-0002=20=E5=B9=B3=E4=BB=93=E5=88=A9=E6=81=AF=E5=A4=8D=E7=8E=B0?= =?UTF-8?q?=E7=94=A8=E4=BE=8B(=E7=9C=9F=E5=AE=9EFR007+=E7=9C=9F=E5=AE=9E?= =?UTF-8?q?=E5=87=BD=E6=95=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调用系统 GetInterests 真实路径(复利/单利两条), 用截图中的真实 FR007 定盘值 在 07-06/07-13 两个重置点真实取数, 复现系统值 -13,667.85 与业务实际值 -13,668.02 - 断言两者差异 0.17 元来自复利第二段本金并入 - 仅保留调用系统函数的断言, 不内嵌手算, 验算见 GLMS20260703_平仓利息验算.xlsx --- .../GLMS20260703CloseInterestTest.cs | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/GLMS20260703CloseInterestTest.cs diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260703CloseInterestTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260703CloseInterestTest.cs new file mode 100644 index 00000000..159fbd83 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GLMS20260703CloseInterestTest.cs @@ -0,0 +1,316 @@ +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 = "511160.SH", + 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 + } +}