From bdba5fbecdf44e3275706d77dd6cd2bb35bb0061 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 14 Aug 2026 10:05:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(EQD-7004):=20=E8=A1=A5=E5=85=A8=E5=88=86?= =?UTF-8?q?=E7=BA=A2=E7=99=BB=E8=AE=B0=E6=97=A5=E5=8F=A3=E5=BE=84=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9A=84=E5=A4=9A=E6=AC=A1=E4=BB=98=E6=81=AF=E7=B4=AF?= =?UTF-8?q?=E8=AE=A1=E6=B5=8B=E8=AF=95=E4=B8=8ETrace=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原修复 9df39491 仅覆盖单次登记日(4/3)的两个单点修复,本次补全 EQD-7004 的完整验证与排查手段: - 测试 GLMS20260105_0006 新增 4 个场景:5 期 reg_date 过滤口径各自命中正确子集;五期票息累计=5x36160=180800;auto 实现归0后下次登记日重新累加;4 期挂账累计读 144640 - 分红读取关键链路加 Debug 级日志(生产可关):BondPaymentService.GetBondPayments(reg_date 过滤区间+命中条数)、SwapDealService.GetPreEodPositionByDate(EOD 定位+回退)、GetPreEodDividendSum(取EOD日期+PosiDividendSum)、方案C 平仓预览/收益结算两处 DividendIn 赋值 实跑 dotnet test 全 GREEN(6/6)。任务编号 EQD-7004。 --- ...S20260105_0006_RegisterDateDividendTest.cs | 123 ++++++++++++++++++ .../Modules/EodModule/BondPaymentService.cs | 2 + .../Modules/SwapModule/SwapDealService.cs | 10 +- 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs index 4ad902b2..cafe7e59 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs @@ -26,6 +26,16 @@ namespace YLErp.Modules.SwapModule private static readonly DateTime PayDate = new(2026, 4, 6); private static readonly DateTime PreRegDate = new(2026, 4, 2); + // 多次付息日历(截图:债券 230004.IB,每期票息 0.1808,共 5 次登记日) + private static readonly DateTime[] RegDates = { + new(2026, 2, 28), new(2026, 4, 3), new(2026, 4, 29), + new(2026, 5, 29), new(2026, 6, 29) + }; + private static readonly DateTime[] PayDates = { + new(2026, 3, 2), new(2026, 4, 6), new(2026, 4, 30), + new(2026, 6, 1), new(2026, 6, 30) + }; + #region 成因 A:日期口径 seam private sealed class TestableBondPaymentService : BondPaymentService @@ -60,6 +70,60 @@ namespace YLErp.Modules.SwapModule "当前按支付日(pay_date_PL=4/6)过滤会漏选->0条,导致分红不计提。"); } + [TestMethod] + public void CauseA_MultiRegDate_跨登记日区间命中正确子集() + { + var records = Enumerable.Range(0, 5).Select(i => new BondPayment + { + underlyingCode = BondCode, + reg_date = RegDates[i], + payment_date_pl = PayDates[i], + payment_date = PayDates[i], + payment_interest = PaymentPer100 + }).ToList(); + var svc = new TestableBondPaymentService(records); + + // 单次窗口:每个登记日各自命中 1 条(验证按 reg_date 过滤,非支付日) + for (int i = 0; i < 5; i++) + { + var prev = i == 0 ? RegDates[i].AddDays(-1) : RegDates[i - 1]; + var hit = svc.GetBondPayments(BondCode, prev, RegDates[i]); + Assert.AreEqual(1, hit.Count, $"窗口({prev:yyyy-MM-dd},{RegDates[i]:yyyy-MM-dd}] 应仅命中登记日 {RegDates[i]:yyyy-MM-dd} 那条"); + Assert.AreEqual(RegDates[i], hit[0].reg_date, "命中的应是该登记日记录"); + } + + // 长区间应命中全部 5 条,不漏不混 + var all = svc.GetBondPayments(BondCode, RegDates[0].AddDays(-1), RegDates[4]); + Assert.AreEqual(5, all.Count, "长区间(登记日1前,登记日5] 应命中全部 5 次付息"); + + // 跨登记日中间区间:(4/2, 4/29] 应命中 4/3 与 4/29 两条(不含 2/28、5/29、6/29) + var mid = svc.GetBondPayments(BondCode, new DateTime(2026, 4, 2), new DateTime(2026, 4, 29)); + Assert.AreEqual(2, mid.Count, "(4/2,4/29] 应命中 4/3+4/29 两条"); + CollectionAssert.AreEquivalent( + new[] { new DateTime(2026, 4, 3), new DateTime(2026, 4, 29) }, + mid.Select(x => x.reg_date!.Value).ToArray()); + } + + [TestMethod] + public void CauseA_MultiRegDate_CalcPayment累加五期票息() + { + var records = Enumerable.Range(0, 5).Select(i => new BondPayment + { + underlyingCode = BondCode, + reg_date = RegDates[i], + payment_date_pl = PayDates[i], + payment_date = PayDates[i], + payment_interest = PaymentPer100 + }).ToList(); + var svc = new TestableBondPaymentService(records); + + // 长区间取全部 5 期,CalcPayment 应累加 = 5 × 36160 = 180,800(原测试仅覆盖单期) + var payments = svc.GetBondPayments(BondCode, RegDates[0].AddDays(-1), RegDates[4]); + var total = svc.CalcPayment(payments, Qty, 1, 1); + Assert.AreEqual(5 * ExpectedDividend, total, 0.01m, + "5 期票息累加应为 5 × 36,160 = 180,800;单期口径会漏计其余 4 期"); + } + #endregion #region 成因 B:T-1 快照 seam @@ -107,6 +171,65 @@ namespace YLErp.Modules.SwapModule "当前 GetPreEodDividendSum 用 ValueDate < dealDate 读 T-1 快照->0。"); } + [TestMethod] + public void CauseB_MultiRegDate_Auto实现归0后下次登记日重新累加() + { + // 模拟:登记日1(2/28)计提 36160 → auto互换实现归0(3/1) → 登记日2(4/3)再计提 36160 + var eodSwaps = new List + { + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,2,27) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,2,28) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,3,1) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,2) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,3) }, + }; + var eodPositions = new List + { + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,2,27), PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,2,28), PosiDividendSum = ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,3,1), PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,2), PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,3), PosiDividendSum = ExpectedDividend, PosiQuantity = Qty }, + }; + var svc = new TestableSwapDealService(eodSwaps, eodPositions); + + // 登记日2(4/3)当天手动互换:应读 4/3 EOD = 36160(第二次,非第一次已实现的、非 0) + var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 3)); + Assert.AreEqual(ExpectedDividend, dividend, 0.01m, + "登记日2(4/3)手动互换应读当日EOD=第二次分红36160;" + + "若读T-1(4/2=0)则漏当日,若读2/28则错取第一次已实现的。"); + } + + [TestMethod] + public void CauseB_MultiRegDate_手动互换期间分红挂账累计四期() + { + // 模拟:多次登记日之间未 auto 实现,分红挂账累加 + // 4/3=36160, 4/29=72320, 5/29=108480, 6/29=144640(4期累计) + var eodSwaps = new List + { + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,3) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,29) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,5,29) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,6,29) }, + }; + var eodPositions = new List + { + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,3), PosiDividendSum = 1 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,29), PosiDividendSum = 2 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,5,29), PosiDividendSum = 3 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,6,29), PosiDividendSum = 4 * ExpectedDividend, PosiQuantity = Qty }, + }; + var svc = new TestableSwapDealService(eodSwaps, eodPositions); + + // 每次登记日当天手动互换应读到该日累计值(验证多次付息累计被正确读取) + Assert.AreEqual(1 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 3)), 0.01m, "4/3 应读 36160"); + Assert.AreEqual(2 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 29)), 0.01m, "4/29 应读 72320(2期累计)"); + Assert.AreEqual(3 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 5, 29)), 0.01m, "5/29 应读 108480(3期累计)"); + // 关键:第 4 期登记日累计 = 4 × 36160 = 144640(原 9df39491 仅覆盖单期 36160,未验证多次付息累计) + Assert.AreEqual(4 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 6, 29)), 0.01m, + "6/29 应读 144640(4期累计);原 9df39491 仅覆盖单期 36160,未验证多次付息累计。"); + } + #endregion } } diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index 3e89b5b4..d601e628 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -103,6 +103,8 @@ namespace YLErp.Modules.EodModule var result = QueryBondPayments(underlyingCode) .Where(x => x.reg_date > startDate && x.reg_date <= endDate) .AsNoTracking().ToList(); + Log.Debug($"[分红-登记日口径] GetBondPayments underlyingCode={underlyingCode} 区间=({startDate:yyyy-MM-dd},{endDate:yyyy-MM-dd}] 按reg_date过滤, 命中 {result.Count} 条: " + + string.Join(",", result.Select(r => r.reg_date?.ToString("yyyy-MM-dd")))); return result; } diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 20af8f9d..21eca92b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -277,6 +277,7 @@ namespace YLErp.Modules.SwapModule // DividendPending = "待结算分红收益"(仍挂在账上、未来才结的存量 = PosiDividendSum 全量口径, // 见 GetPreEodDividendSum 注释的口径论证;切勿改回硬0或分摊,会落库回归) decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate); + Logger.Debug($"[分红-平仓预览] 方案C DividendIn=DividendPending=PosiDividendSum全量 tradeId={tradeId} positionId={position.PositionId} dealDate={dealDate:yyyy-MM-dd} 值={preEodDividendSum}"); floatEvent.DividendIn = preEodDividendSum; floatEvent.DividendPending = preEodDividendSum; floatEvent.UnderlyingCode = position.UnderlyingCode; @@ -410,7 +411,9 @@ namespace YLErp.Modules.SwapModule floatEvent.PositionId = position.PositionId; // 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源), // 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。 - floatEvent.DividendIn = GetPreEodDividendSum(tradeId, position.PositionId, dealDate); + decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate); + Logger.Debug($"[分红-收益结算] DividendIn=PosiDividendSum全量 tradeId={tradeId} positionId={position.PositionId} dealDate={dealDate:yyyy-MM-dd} 值={preEodDividendSum}"); + floatEvent.DividendIn = preEodDividendSum; floatEvent.UnderlyingCode = position.UnderlyingCode; floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType; floatEvent.CloseFee = 0; @@ -807,7 +810,9 @@ namespace YLErp.Modules.SwapModule protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate) { var preEod = GetPreEodPositionByDate(tradeId, positionId, dealDate); - return preEod == null ? 0m : preEod.PosiDividendSum; + var sum = preEod == null ? 0m : preEod.PosiDividendSum; + Logger.Debug($"[分红-读取] GetPreEodDividendSum tradeId={tradeId} positionId={positionId} dealDate={dealDate:yyyy-MM-dd} 取EOD日期={(preEod?.ValueDate):yyyy-MM-dd} PosiDividendSum={sum}"); + return sum; } /// @@ -821,6 +826,7 @@ namespace YLErp.Modules.SwapModule .Where(x => x.ValueDate <= dealDate) .OrderByDescending(o => o.ValueDate).FirstOrDefault(); var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate; + Logger.Debug($"[分红-快照定位] GetPreEodPositionByDate tradeId={tradeId} positionId={positionId} dealDate={dealDate:yyyy-MM-dd} 取<=当日EOD, 命中日期={(lastEod?.ValueDate):yyyy-MM-dd}, 回退={lastEod == null}"); return QueryPreEodPosition(tradeId, positionId, preEodDate); }