From ac7b04e79dc1c17cddce18c9f4ec893bdf809748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=90=8D=E9=94=90?= <1565842059@qq.com> Date: Wed, 26 Aug 2026 13:40:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(bond):=20#JTZG-136=20=E5=9B=BD=E8=81=94?= =?UTF-8?q?=E6=B0=91=E7=94=9F-=E6=9C=89=E6=9C=9F=E9=97=B4=E4=BB=98?= =?UTF-8?q?=E6=81=AF=EF=BC=8C=E8=87=AA=E5=8A=A8=E4=BA=92=E6=8D=A2=E7=9A=84?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=E4=BA=92=E6=8D=A2=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 BondPaymentService.GetBondPayments 方法的过滤条件从 reg_date 改为 payment_date - 更新相关日志信息显示 [分红-付息日口径] 替代 [分红-登记日口径] - 修正单元测试中的注释和变量名,统一使用 payment_date 口径描述 - 修改测试数据构造,验证按实际付息日进行票息计提的逻辑 - 更新相关文档注释,明确日期口径变更的具体实现方式 --- .../DividendEodNoDoubleCountTest.cs | 49 +++--- ...S20260105_0006_RegisterDateDividendTest.cs | 142 ++++++++---------- .../SwapModule/RegDateDividendEodE2ETest.cs | 122 +++++++-------- .../Modules/EodModule/BondPaymentService.cs | 15 +- 4 files changed, 143 insertions(+), 185 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/DividendEodNoDoubleCountTest.cs b/UnitTestProject/Modules/SwapModule/DividendEodNoDoubleCountTest.cs index bb9af66d..d70f92f7 100644 --- a/UnitTestProject/Modules/SwapModule/DividendEodNoDoubleCountTest.cs +++ b/UnitTestProject/Modules/SwapModule/DividendEodNoDoubleCountTest.cs @@ -22,14 +22,14 @@ namespace YLErp.Modules.SwapModule private const decimal RegPer100 = 1.0m; // 每 100 元面值票息 1.0 → qty(1000) 时单期分红 = 1.0×1000/100 = 10 private static readonly DateTime StartDate = new(2026, 1, 5); - #region 内存债券付息数据(reg_date 口径,真实生产 GetBondPayments 读取) + #region 内存债券付息数据(payment_date 口径,真实生产 GetBondPayments 读取) private const string BondUnderlying = "210210.IB"; private static List BondPayments() => new List { - // 登记日 1/6、1/7 各一期;支付日滞后若干日(刻意与登记日不同,验证按 reg_date 而非 pay_date 计提) - new BondPayment { underlyingCode = BondUnderlying, reg_date = new DateTime(2026, 1, 6), payment_date_pl = new DateTime(2026, 1, 9), payment_date = new DateTime(2026, 1, 9), payment_interest = RegPer100 }, - new BondPayment { underlyingCode = BondUnderlying, reg_date = new DateTime(2026, 1, 7), payment_date_pl = new DateTime(2026, 1, 10), payment_date = new DateTime(2026, 1, 10), payment_interest = RegPer100 }, + // 付息日 1/6、1/7 各一期 + new BondPayment { underlyingCode = BondUnderlying, reg_date = new DateTime(2026, 1, 5), payment_date_pl = new DateTime(2026, 1, 6), payment_date = new DateTime(2026, 1, 6), payment_interest = RegPer100 }, + new BondPayment { underlyingCode = BondUnderlying, reg_date = new DateTime(2026, 1, 6), payment_date_pl = new DateTime(2026, 1, 7), payment_date = new DateTime(2026, 1, 7), payment_interest = RegPer100 }, }; #endregion @@ -51,7 +51,7 @@ namespace YLErp.Modules.SwapModule => _eodPositions.FirstOrDefault(x => x.SwapTradeId == tradeId && x.PositionId == positionId && x.ValueDate == valueDate); } - /// 真实 BondPaymentService(reg_date 口径)seam:仅注入内存 BondPayment 数据,票息计算走生产 GetBondPayments+CalcPayment。 + /// 真实 BondPaymentService(payment_date 口径)seam:仅注入内存 BondPayment 数据,票息计算走生产 GetBondPayments+CalcPayment。 private sealed class RealBondPaymentService : BondPaymentService { private readonly List _data; @@ -60,14 +60,14 @@ namespace YLErp.Modules.SwapModule => _data.Where(x => x.underlyingCode == underlyingCode).AsQueryable(); } - /// SwapEodPositionService stub:暴露 UpdateEodPosition/CopyEodPosition;CalcBondPayment 桥接真实 BondPaymentService(reg_date 口径,不再用线性假公式)。 + /// SwapEodPositionService stub:暴露 UpdateEodPosition/CopyEodPosition;CalcBondPayment 桥接真实 BondPaymentService(payment_date 口径)。 private sealed class EodSvcStub : TestableSwapEodPositionService { private readonly List _bondPayments; public EodSvcStub(List bondPayments) : base(nameof(DividendEodNoDoubleCountTest)) { _bondPayments = bondPayments; } protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) { - // 桥接真实生产口径:GetBondPayments 按 reg_date 过滤 + CalcPayment 累加(替换原线性假公式 DailyRatePerUnit*days*qty) + // 桥接真实生产口径:GetBondPayments 按 payment_date 过滤 + CalcPayment 累加 var svc = new RealBondPaymentService(_bondPayments, OptUserInfo.UnitTestUser); return svc.CalcPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); } @@ -147,9 +147,9 @@ namespace YLErp.Modules.SwapModule /// 盘中收益互换:DividendIn 由 GetPreEodDividendSum 真实算(读 T-1 EOD)→ 保存 → EOD。 /// 验证:不重复(EOD TdCloseDividend 扣 DividendIn)+ 不丢失(当日新计进 PosiDividendSum)+ 守恒。 /// - /// 序列(StartDate=1/5,reg_date 1/6、1/7 各一期,每期 = qty×per100/100 = 10): - /// D1=1/6 无事件 Copy:窗口(1/5,1/6] 命中 reg_date 1/6 → TdPosiDividend=10,PosiDividendSum=10 - /// D2=1/7 盘中互换:GetPreEodDividendSum(读 D1) → DividendIn=10;保存 swap_event;EOD 窗口(1/6,1/7] 命中 reg_date 1/7 → 新计 10 - 实现 10 → PosiDividendSum=10 + /// 序列(StartDate=1/5,payment_date 1/6、1/7 各一期,每期 = qty×per100/100 = 10): + /// D1=1/6 无事件 Copy:窗口(1/5,1/6] 命中 payment_date 1/6 → TdPosiDividend=10,PosiDividendSum=10 + /// D2=1/7 盘中互换:GetPreEodDividendSum(读 D1) → DividendIn=10;保存 swap_event;EOD 窗口(1/6,1/7] 命中 payment_date 1/7 → 新计 10 - 实现 10 → PosiDividendSum=10 /// 守恒:全程新计(10+10) - 全程实现(10) = 末尾 PosiDividendSum(10) /// [TestMethod] @@ -196,16 +196,10 @@ namespace YLErp.Modules.SwapModule } /// - /// 登记日当日全平(盘中平仓→收盘持仓 0):按各交易场所规定,不享有登记日当日的分红 - /// (股权登记日以收盘在册为准;盘中全平→收盘不在册)。验证系统行为符合该规定。 - /// - /// 系统行为:①盘中 DividendIn=GetPreEodDividendSum 读 T-1(=T日前待实现,正确不含登记日当日 reg_date 1/7 的分红); - /// ②EOD 全平 PosiQuantity=0 → TdPosiDividend=0(不计提登记日当日 reg_date 1/7)+ PosiDividendSum=0。 - /// 即登记日当日分红(reg_date 1/7 的 10)既不进 DividendIn、也不进 PosiDividendSum = 正确不享有。 - /// 应得 = T日前待实现累计(r1.PosiDividendSum,仅含 1/6 那期 10);实拿 = DividendIn → 相等,无丢失(不享有当日是正确的)。 + /// 付息日当日全平(盘中平仓→收盘持仓 0):按各交易场所规定,不享有付息日当日的分红。 /// [TestMethod] - public void 登记日全平_按交易场所规定不享有当日分红() + public void 付息日全平_按交易场所规定不享有当日分红() { var eodSvc = new EodSvcStub(BondPayments()); var td = CreateTrade(); @@ -231,30 +225,21 @@ namespace YLErp.Modules.SwapModule // D2=1/7 EOD(UpdateEodPosition,全平→PosiQuantity=0) var r2 = eodSvc.ExecuteUpdateEodPosition(position, r1, td, d2, d1, new List { closeEvent }); - // 业务规定:登记日当日全平(盘中平仓→收盘持仓为 0),按各交易场所规定不享有登记日当日的分红 - // (股权登记日以收盘在册为准)。故应得 = T日(登记日)之前的待实现累计 = r1.PosiDividendSum(不含登记日当日)。 - // 系统行为正确:①DividendIn 读 T-1(=T日前待实现,正确不含当日);②EOD 全平 PosiQuantity=0 不计提当日。 - // 即登记日当日分红既不进 DividendIn 也不进 PosiDividendSum = 正确不享有。 - decimal expectedTotal = r1.PosiDividendSum; // 应得 = T日前待实现(不含登记日当日,因全平不享有) + decimal expectedTotal = r1.PosiDividendSum; decimal actualGot = dividendIn + r2.PosiDividendSum; - Console.WriteLine($"[登记日全平] 应得(T日前待实现)={expectedTotal}, 实拿(DividendIn+PosiDividendSum)={actualGot}"); - Console.WriteLine($"[登记日全平] DividendIn={dividendIn}, EOD:TdPosiDividend={r2.TdPosiDividend} PosiDividendSum={r2.PosiDividendSum} PosiQuantity={r2.PosiQuantity}"); + Console.WriteLine($"[付息日全平] 应得(T日前待实现)={expectedTotal}, 实拿(DividendIn+PosiDividendSum)={actualGot}"); + Console.WriteLine($"[付息日全平] DividendIn={dividendIn}, EOD:TdPosiDividend={r2.TdPosiDividend} PosiDividendSum={r2.PosiDividendSum} PosiQuantity={r2.PosiQuantity}"); - // 断言:实拿 = 应得(登记日全平不享有当日,符合交易场所规定) AssertDecimalEqual(expectedTotal, actualGot, 0.01m, - $"实拿应=应得(T日前待实现{expectedTotal}),登记日全平不享有当日分红(符合交易场所规定)"); - AssertDecimalEqual(0m, r2.TdPosiDividend, 0.01m, "登记日全平 EOD 不计提当日(PosiQuantity=0,正确)"); + $"实拿应=应得(T日前待实现{expectedTotal}),付息日全平不享有当日分红"); + AssertDecimalEqual(0m, r2.TdPosiDividend, 0.01m, "付息日全平 EOD 不计提当日(PosiQuantity=0,正确)"); AssertDecimalEqual(0m, r2.PosiDividendSum, 0.01m, "全平后 PosiDividendSum=0"); } /// /// 【死代码删除的边界规格】脏数据(OriginalStockEqvNotional=null / PosiNetPrice=0)不得让 /// UpdateEodPosition 崩溃,且分红产出与正常数据完全一致。 - /// 背景:这两个字段在 UpdateEodPosition 内的唯一消费点是历史遗留死代码 - /// (originNotional→totalPayment 全历史重算,结果从未被使用,2026-08 论证后删除)—— - /// 删除前该脏数据会在 EOD 抛 InvalidOperationException/除零;删除后是设计内行为。 - /// 本测试同时钉住:删除后输出等价(与同输入正常数据路径一致)。 /// [TestMethod] public void 脏数据边界_死代码涉及字段_不影响EOD分红产出() diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs index cafe7e59..c2529836 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs @@ -3,13 +3,10 @@ using YLErp.Modules.EodModule; namespace YLErp.Modules.SwapModule { /// - /// GLMS-20260105-0006 回归:债券 TRS 登记日当天手动平仓/互换,分红收益应为 36160 而非 0。 - /// 根因双成因: - /// A. BondPaymentService.GetBondPayments 用支付日(pay_date_PL/pay_date_act)而非债权登记日(reg_date)判定谁享有票息 - /// -> 登记日(4/3)当日 EOD 不计提,跨过支付日(4/6)才计提(巧合:4/4-4/5周末,下一交易日恰=支付日,掩盖缺陷) - /// B. SwapDealService.GetPreEodDividendSum 用 ValueDate 严格小于 dealDate 读 T-1 EOD 快照 - /// -> 登记日当天手动平仓读不到当日 EOD,拿到 0 - /// 本文件用手工合成内存数据(不连 96 库),通过 virtual seam 注入,真实跑生产日期逻辑。 + /// 债券 TRS 付息日(payment_date)分红测试。 + /// 验证: + /// A. BondPaymentService.GetBondPayments 用实际付息日(payment_date)判定付息归属 + /// B. SwapDealService.GetPreEodDividendSum 读 EOD 快照 /// [TestClass] public class GLMS20260105_0006_RegisterDateDividendTest @@ -21,12 +18,12 @@ namespace YLErp.Modules.SwapModule private const decimal PaymentPer100 = 0.1808m; private const decimal ExpectedDividend = 36_160m; // 20,000,000 × 0.1808 / 100 - // 付息日历(截图):登记日 4/3,支付日 4/6 + // 付息日历:登记日 4/3,支付日 4/6 private static readonly DateTime RegDate = new(2026, 4, 3); private static readonly DateTime PayDate = new(2026, 4, 6); - private static readonly DateTime PreRegDate = new(2026, 4, 2); + private static readonly DateTime PrePayDate = new(2026, 4, 5); - // 多次付息日历(截图:债券 230004.IB,每期票息 0.1808,共 5 次登记日) + // 多次付息日历:债券 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) @@ -48,30 +45,28 @@ namespace YLErp.Modules.SwapModule } [TestMethod] - public void CauseA_登记日当日EOD_应按登记日口径选中付息记录() + public void CauseA_付息日当日EOD_应按付息日口径选中付息记录() { var record = new BondPayment { underlyingCode = BondCode, - reg_date = RegDate, // 债权登记日 4/3(关键:分红归属按此判定) + reg_date = RegDate, // 债权登记日 4/3 payment_date_pl = PayDate, // 理论付息日 4/6 payment_date = PayDate, // 实际付息日 4/6 payment_interest = PaymentPer100 }; var svc = new TestableBondPaymentService(new List { record }); - // 登记日当日的 EOD 计提区间 (4/2, 4/3] - var payments = svc.GetBondPayments(BondCode, PreRegDate, RegDate); + // 付息日当日的 EOD 计提区间 (4/5, 4/6] + var payments = svc.GetBondPayments(BondCode, PrePayDate, PayDate); - // 修复前:用支付日(pay_date_PL=4/6)过滤 -> 4/6 不在 (4/2,4/3] -> 0 条(漏计分红) - // 修复后:用债权登记日(reg_date=4/3)过滤 -> 4/3 落在区间 -> 1 条(GLMS-20260105-0006 已修复) + // 按支付日(payment_date=4/6)过滤 -> 4/6 落在区间 -> 1 条 Assert.AreEqual(1, payments.Count, - "登记日(4/3)当日 EOD 应按债权登记日(reg_date)选中该笔付息;" + - "当前按支付日(pay_date_PL=4/6)过滤会漏选->0条,导致分红不计提。"); + "付息日(4/6)当日 EOD 应按实际付息日(payment_date)选中该笔付息。"); } [TestMethod] - public void CauseA_MultiRegDate_跨登记日区间命中正确子集() + public void CauseA_MultiRegDate_跨付息日区间命中正确子集() { var records = Enumerable.Range(0, 5).Select(i => new BondPayment { @@ -83,25 +78,25 @@ namespace YLErp.Modules.SwapModule }).ToList(); var svc = new TestableBondPaymentService(records); - // 单次窗口:每个登记日各自命中 1 条(验证按 reg_date 过滤,非支付日) + // 单次窗口:每个付息日各自命中 1 条(验证按 payment_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, "命中的应是该登记日记录"); + var prev = i == 0 ? PayDates[i].AddDays(-1) : PayDates[i - 1]; + var hit = svc.GetBondPayments(BondCode, prev, PayDates[i]); + Assert.AreEqual(1, hit.Count, $"窗口({prev:yyyy-MM-dd},{PayDates[i]:yyyy-MM-dd}] 应仅命中付息日 {PayDates[i]:yyyy-MM-dd} 那条"); + Assert.AreEqual(PayDates[i], hit[0].payment_date, "命中的应是该付息日记录"); } // 长区间应命中全部 5 条,不漏不混 - var all = svc.GetBondPayments(BondCode, RegDates[0].AddDays(-1), RegDates[4]); - Assert.AreEqual(5, all.Count, "长区间(登记日1前,登记日5] 应命中全部 5 次付息"); + var all = svc.GetBondPayments(BondCode, PayDates[0].AddDays(-1), PayDates[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 两条"); + // 跨付息日中间区间:(4/5, 4/30] 应命中 4/6 与 4/30 两条(不含 3/2、6/1、6/30) + var mid = svc.GetBondPayments(BondCode, new DateTime(2026, 4, 5), new DateTime(2026, 4, 30)); + Assert.AreEqual(2, mid.Count, "(4/5,4/30] 应命中 4/6+4/30 两条"); CollectionAssert.AreEquivalent( - new[] { new DateTime(2026, 4, 3), new DateTime(2026, 4, 29) }, - mid.Select(x => x.reg_date!.Value).ToArray()); + new[] { new DateTime(2026, 4, 6), new DateTime(2026, 4, 30) }, + mid.Select(x => x.payment_date!.Value).ToArray()); } [TestMethod] @@ -117,8 +112,8 @@ namespace YLErp.Modules.SwapModule }).ToList(); var svc = new TestableBondPaymentService(records); - // 长区间取全部 5 期,CalcPayment 应累加 = 5 × 36160 = 180,800(原测试仅覆盖单期) - var payments = svc.GetBondPayments(BondCode, RegDates[0].AddDays(-1), RegDates[4]); + // 长区间取全部 5 期,CalcPayment 应累加 = 5 × 36160 = 180,800 + var payments = svc.GetBondPayments(BondCode, PayDates[0].AddDays(-1), PayDates[4]); var total = svc.CalcPayment(payments, Qty, 1, 1); Assert.AreEqual(5 * ExpectedDividend, total, 0.01m, "5 期票息累加应为 5 × 36,160 = 180,800;单期口径会漏计其余 4 期"); @@ -146,88 +141,83 @@ namespace YLErp.Modules.SwapModule } [TestMethod] - public void CauseB_登记日当天手动平仓_应读到当日EOD分红36160() + public void CauseB_付息日当天手动平仓_应读到当日EOD分红36160() { - // 4/2 EOD:累计分红 0;4/3 EOD(登记日):累计分红 36160(即登记日应有的状态) + // 4/5 EOD:累计分红 0;4/6 EOD(付息日):累计分红 36160 var eodSwaps = new List { - new eod_swap { SwapTradeId = TradeId, ValueDate = PreRegDate }, - new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } + new eod_swap { SwapTradeId = TradeId, ValueDate = PrePayDate }, + new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate } }; var eodPositions = new List { - new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = PreRegDate, PosiDividendSum = 0m, PosiQuantity = Qty }, - new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = RegDate, PosiDividendSum = ExpectedDividend, PosiQuantity = Qty } + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = PrePayDate, PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = PayDate, PosiDividendSum = ExpectedDividend, PosiQuantity = Qty } }; var svc = new TestableSwapDealService(eodSwaps, eodPositions); - // 登记日(4/3)当天手动平仓 - var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, RegDate); + // 付息日(4/6)当天手动平仓 + var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, PayDate); - // 修复前:ValueDate 严格小于 dealDate 读 T-1(4/2) -> 0(漏读当日分红) - // 修复后:ValueDate 小于等于 dealDate 读当日(4/3) -> 36160(GLMS-20260105-0006 已修复) Assert.AreEqual(ExpectedDividend, dividend, 0.01m, - "登记日(4/3)当天手动平仓应读到当日 EOD 累计分红 36,160;" + - "当前 GetPreEodDividendSum 用 ValueDate < dealDate 读 T-1 快照->0。"); + "付息日(4/6)当天手动平仓应读到当日 EOD 累计分红 36,160。"); } [TestMethod] - public void CauseB_MultiRegDate_Auto实现归0后下次登记日重新累加() + public void CauseB_MultiRegDate_Auto实现归0后下次付息日重新累加() { - // 模拟:登记日1(2/28)计提 36160 → auto互换实现归0(3/1) → 登记日2(4/3)再计提 36160 + // 模拟:付息日1(3/2)计提 36160 → auto互换实现归0(3/3) → 付息日2(4/6)再计提 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) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,3,2) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,3,3) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,5) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,6) }, }; 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 }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,3,2), PosiDividendSum = ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,3,3), PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,5), PosiDividendSum = 0m, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,6), 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)); + // 付息日2(4/6)当天手动互换:应读 4/6 EOD = 36160(第二次,非第一次已实现的、非 0) + var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 6)); Assert.AreEqual(ExpectedDividend, dividend, 0.01m, - "登记日2(4/3)手动互换应读当日EOD=第二次分红36160;" + - "若读T-1(4/2=0)则漏当日,若读2/28则错取第一次已实现的。"); + "付息日2(4/6)手动互换应读当日EOD=第二次分红36160。"); } [TestMethod] public void CauseB_MultiRegDate_手动互换期间分红挂账累计四期() { - // 模拟:多次登记日之间未 auto 实现,分红挂账累加 - // 4/3=36160, 4/29=72320, 5/29=108480, 6/29=144640(4期累计) + // 模拟:多次付息日之间未 auto 实现,分红挂账累加 + // 4/6=36160, 4/30=72320, 6/1=108480, 6/30=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) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,6) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,4,30) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,6,1) }, + new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,6,30) }, }; 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 }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,6), PosiDividendSum = 1 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,4,30), PosiDividendSum = 2 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,6,1), PosiDividendSum = 3 * ExpectedDividend, PosiQuantity = Qty }, + new eod_swap_position { SwapTradeId = TradeId, PositionId = PositionId, ValueDate = new DateTime(2026,6,30), 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,未验证多次付息累计。"); + // 每次付息日当天手动互换应读到该日累计值(验证多次付息累计被正确读取) + Assert.AreEqual(1 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 6)), 0.01m, "4/6 应读 36160"); + Assert.AreEqual(2 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 30)), 0.01m, "4/30 应读 72320(2期累计)"); + Assert.AreEqual(3 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 6, 1)), 0.01m, "6/1 应读 108480(3期累计)"); + Assert.AreEqual(4 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 6, 30)), 0.01m, + "6/30 应读 144640(4期累计)。"); } #endregion diff --git a/UnitTestProject/Modules/SwapModule/RegDateDividendEodE2ETest.cs b/UnitTestProject/Modules/SwapModule/RegDateDividendEodE2ETest.cs index 38b58df9..8e68acfa 100644 --- a/UnitTestProject/Modules/SwapModule/RegDateDividendEodE2ETest.cs +++ b/UnitTestProject/Modules/SwapModule/RegDateDividendEodE2ETest.cs @@ -8,16 +8,7 @@ using System.Linq; namespace YLErp.Modules.SwapModule { /// - /// GLMS-20260105-0006 端到端补充:EOD 分红引擎的票息归属须按【债权登记日 reg_date】判定, - /// 而非支付日(pay_date)。此前 DividendEodNoDoubleCountTest.EodSvcStub 把 CalcBondPayment 覆写成 - /// 线性公式(DailyRatePerUnit*days*qty),**绕开了 reg_date 口径**——即没有真正验证"引擎按登记日计提"。 - /// - /// 本文件把 EOD stub 的 CalcBondPayment seam 重新桥接回【真实的 BondPaymentService(reg_date 口径)】, - /// 仅用内存 BondPayment 数据(不连库),使端到端流程(CopyEodPosition/UpdateEodPosition + GetPreEodDividendSum) - /// 真正跑生产日期逻辑: - /// ① EOD 引擎在登记日计提、支付日不计提(证明 reg_date 口径); - /// ② 登记日下一日(T+1)全平:经 GetPreEodDividendSum 读到登记日当日 EOD 分红(收盘在册→享有); - /// ③ 部分平仓 T+1:DividendIn 为全量(非按比例缩放),剩余 PosiDividendSum 归 0(记录当前生产行为)。 + /// EOD 分红引擎票息归属按【实际付息日 payment_date】判定测试。 /// [TestClass] public class RegDateDividendEodE2ETest @@ -31,9 +22,9 @@ namespace YLErp.Modules.SwapModule private static readonly DateTime StartDate = new(2026, 4, 1); private static readonly DateTime RegDate = new(2026, 4, 3); // 债权登记日 - private static readonly DateTime PayDate = new(2026, 4, 6); // 实际支付日(与登记日差 3 天) + private static readonly DateTime PayDate = new(2026, 4, 6); // 实际支付日 - #region 内存债券付息数据(reg_date 口径) + #region 内存债券付息数据(payment_date 口径) private static List BondPayments() => new List @@ -41,16 +32,16 @@ namespace YLErp.Modules.SwapModule new BondPayment { underlyingCode = BondCode, - reg_date = RegDate, // 关键:分红归属按债权登记日判定 - payment_date_pl = PayDate, // 理论付息日(非归属口径) - payment_date = PayDate, // 实际付息日(非归属口径) + reg_date = RegDate, + payment_date_pl = PayDate, + payment_date = PayDate, // 实际付息日 payment_interest = PaymentPer100 } }; #endregion - #region BondPaymentService seam(桥接真实 reg_date 口径,内存数据) + #region BondPaymentService seam(桥接真实 payment_date 口径,内存数据) private sealed class RegDateBondPaymentService : BondPaymentService { @@ -71,7 +62,7 @@ namespace YLErp.Modules.SwapModule protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) { - // 桥接真实生产口径:BondPaymentService.GetBondPayments 按 reg_date 过滤 + CalcPayment 累加 + // 桥接真实生产口径:BondPaymentService.GetBondPayments 按 payment_date 过滤 + CalcPayment 累加 var svc = new RegDateBondPaymentService(_bondPayments, OptUserInfo.UnitTestUser); return svc.CalcPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio); } @@ -113,7 +104,7 @@ namespace YLErp.Modules.SwapModule private static trade CreateTrade() => new trade { - id = TradeId, TradeNumber = "UT-REGDATE-E2E-001", ClientId = 999999, + id = TradeId, TradeNumber = "UT-PAYDATE-E2E-001", ClientId = 999999, TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, ExerciseDate = new DateTime(2027, 4, 1), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", @@ -161,81 +152,74 @@ namespace YLErp.Modules.SwapModule #endregion /// - /// 端到端证 reg_date 口径:EOD 引擎(CopyEodPosition)逐日计提时, - /// 仅在【债权登记日】产生分红,【支付日】不产生(即便支付日与登记日相差数日)。 - /// 这是线性 stub 无法覆盖的——线性公式按"天数"算,永远无法区分登记日 vs 支付日。 + /// 端到端证 payment_date 口径:EOD 引擎(CopyEodPosition)逐日计提时, + /// 仅在【实际付息日】产生分红,【登记日/非付息日】不产生。 /// [TestMethod] - public void 登记日口径_EOD引擎按reg_date计提_非pay_date() + public void 付息日口径_EOD引擎按payment_date计提() { var eodSvc = new RegDateEodStub(BondPayments()); var td = CreateTrade(); var initialEod = CreateInitialEod(); - // D1=4/2(登记日前一日):窗口 (4/1,4/2] 无登记日 → 0 + // D1=4/2:窗口 (4/1,4/2] 无付息日 → 0 var r1 = eodSvc.ExecuteCopyEodPosition(initialEod, td, new DateTime(2026, 4, 2), StartDate); - AssertDecimalEqual(0m, r1.TdPosiDividend, 0.01m, "4/2 当日新计(无登记日)"); - AssertDecimalEqual(0m, r1.PosiDividendSum, 0.01m, "4/2 累计(无登记日)"); + AssertDecimalEqual(0m, r1.TdPosiDividend, 0.01m, "4/2 当日新计(无付息日)"); + AssertDecimalEqual(0m, r1.PosiDividendSum, 0.01m, "4/2 累计(无付息日)"); - // D2=4/3(登记日):窗口 (4/2,4/3] 命中 reg_date=4/3 → 36160 + // D2=4/3(登记日,非付息日):窗口 (4/2,4/3] 无付息日 → 0 var r2 = eodSvc.ExecuteCopyEodPosition(r1, td, RegDate, StartDate); - AssertDecimalEqual(ExpectedDividend, r2.TdPosiDividend, 0.01m, - "4/3 登记日当日应计提 36160(按 reg_date 口径);若按支付日(pay_date=4/6)则此处为 0(漏计)。"); - AssertDecimalEqual(ExpectedDividend, r2.PosiDividendSum, 0.01m, "4/3 累计=36160"); + AssertDecimalEqual(0m, r2.TdPosiDividend, 0.01m, "4/3 登记日(非付息日)不计提"); + AssertDecimalEqual(0m, r2.PosiDividendSum, 0.01m, "4/3 累计=0"); - // D3=4/6(支付日,非登记日):窗口 (4/3,4/6] 不含任何 reg_date(4/3 不>4/3;4/6 是支付日非登记日)→ 0 + // D3=4/6(付息日):窗口 (4/3,4/6] 命中 payment_date=4/6 → 36160 var r3 = eodSvc.ExecuteCopyEodPosition(r2, td, PayDate, StartDate); - AssertDecimalEqual(0m, r3.TdPosiDividend, 0.01m, - "4/6 支付日不应计提(分红归属按 reg_date,不是 pay_date);线性 stub 因按天数算会在此误计。"); - AssertDecimalEqual(ExpectedDividend, r3.PosiDividendSum, 0.01m, "4/6 累计仍为 36160(支付日不重复计提)"); + AssertDecimalEqual(ExpectedDividend, r3.TdPosiDividend, 0.01m, "4/6 付息日当日应计提 36160"); + AssertDecimalEqual(ExpectedDividend, r3.PosiDividendSum, 0.01m, "4/6 累计为 36160"); - Console.WriteLine($"[reg_date 口径] 4/2={r1.PosiDividendSum}, 4/3={r2.PosiDividendSum}(登记日计提), 4/6={r3.PosiDividendSum}(支付日不计提)"); + Console.WriteLine($"[payment_date 口径] 4/2={r1.PosiDividendSum}, 4/3={r2.PosiDividendSum}, 4/6={r3.PosiDividendSum}(付息日计提)"); } /// - /// 用户场景「登记日下一日(T+1)全平」:T日(登记日)收盘在册→享有T日分红; - /// T+1盘中全平,GetPreEodDividendSum(T+1) 应读到 T日 EOD(含当日分红)= 36160,而非漏读为 0。 - /// 验证端到端:EOD 引擎算出 T日分红 → 快照 → 手动/互换读取正确取到。 + /// 用户场景「付息日下一日(T+1)全平」:T日(付息日)收盘在册→享有T日分红; + /// T+1盘中全平,GetPreEodDividendSum(T+1) 应读到 T日 EOD(含当日分红)= 36160。 /// [TestMethod] - public void 登记日下一日全平_经GetPreEodDividendSum读到登记日分红() + public void 付息日下一日全平_经GetPreEodDividendSum读到付息日分红() { var eodSvc = new RegDateEodStub(BondPayments()); var td = CreateTrade(); var position = CreatePosition(); var initialEod = CreateInitialEod(); - // T日=4/3(登记日)EOD:引擎算出分红 36160(reg_date 口径) - var rReg = eodSvc.ExecuteCopyEodPosition(initialEod, td, RegDate, StartDate); - AssertDecimalEqual(ExpectedDividend, rReg.PosiDividendSum, 0.01m, "登记日 T日 EOD 累计分红=36160"); + // T日=4/6(付息日)EOD:引擎算出分红 36160(payment_date 口径) + var rPay = eodSvc.ExecuteCopyEodPosition(initialEod, td, PayDate, StartDate); + AssertDecimalEqual(ExpectedDividend, rPay.PosiDividendSum, 0.01m, "付息日 T日 EOD 累计分红=36160"); - // T+1=4/4 盘中:注入 T日 EOD 快照,GetPreEodDividendSum 应读 T日(<=当日) → 36160 + // T+1=4/7 盘中:注入 T日 EOD 快照,GetPreEodDividendSum 应读 T日(<=当日) → 36160 var dealSvc = new DealSvcStub( - new List { new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } }, - new List { rReg }); - decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 4)); + new List { new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate } }, + new List { rPay }); + decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 7)); AssertDecimalEqual(ExpectedDividend, dividendIn, 0.01m, - "T+1(4/4) 盘中全平应经 GetPreEodDividendSum 读到 T日(4/3)EOD 分红 36160(收盘在册→享有);" + - "若 < 严格小于 dealDate 读 T-1(4/2=0) 则漏读登记日当日。"); + "T+1(4/7) 盘中全平应经 GetPreEodDividendSum 读到 T日(4/6)EOD 分红 36160"); Console.WriteLine($"[T+1 全平] DividendIn(读T日EOD)={dividendIn}"); - // T+1=4/4 EOD 全平:PosiQuantity=0 → 不计提当日 + PosiDividendSum 归 0 - var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rReg, td, new DateTime(2026, 4, 4), RegDate, - new List { CloseEvent(Qty, dividendIn, new DateTime(2026, 4, 4)) }); + // T+1=4/7 EOD 全平:PosiQuantity=0 → 不计提当日 + PosiDividendSum 归 0 + var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rPay, td, new DateTime(2026, 4, 7), PayDate, + new List { CloseEvent(Qty, dividendIn, new DateTime(2026, 4, 7)) }); - // 实拿 = DividendIn(本次落袋) + 末尾 PosiDividendSum(剩余挂账) = 应得(T日前待实现=持有至登记日) decimal actualGot = dividendIn + rT1.PosiDividendSum; - AssertDecimalEqual(ExpectedDividend, actualGot, 0.01m, "实拿=应得(持有至登记日享有的 36160)"); - AssertDecimalEqual(0m, rT1.TdPosiDividend, 0.01m, "T+1 非登记日,EOD 不计提当日"); + AssertDecimalEqual(ExpectedDividend, actualGot, 0.01m, "实拿=应得 36160"); + AssertDecimalEqual(0m, rT1.TdPosiDividend, 0.01m, "T+1 非付息日,EOD 不计提当日"); AssertDecimalEqual(0m, rT1.PosiDividendSum, 0.01m, "全平后 PosiDividendSum=0"); Console.WriteLine($"[T+1 全平] 应得={ExpectedDividend}, 实拿={actualGot}, 末尾PosiDividendSum={rT1.PosiDividendSum}"); } /// - /// 部分平仓 T+1:当前生产行为记录(非修复目标)。 - /// T日(登记日)持有→T+1盘中部分平仓:GetPreEodDividendSum 返回的是【全量】待实现分红(非按平仓比例缩放), - /// 故 DividendIn=全量 36160;T+1 EOD 部分平仓(PosiQuantity>0)后剩余 PosiDividendSum=前日-全量=0。 - /// 注:此"DividendIn 不按平仓比例缩放"是当前生产行为,已与用户确认(潜在一致性议题,非本 bug 修复范围)。 + /// 部分平仓 T+1: + /// T日(付息日)持有→T+1盘中部分平仓:GetPreEodDividendSum 返回的是全量待实现分红, + /// 故 DividendIn=全量 36160;T+1 EOD 部分平仓后剩余 PosiDividendSum=前日-全量=0。 /// [TestMethod] public void 部分平仓_T1_DividendIn为全量_剩余PosiDividendSum归0() @@ -245,25 +229,25 @@ namespace YLErp.Modules.SwapModule var position = CreatePosition(); var initialEod = CreateInitialEod(); - // T日=4/3(登记日)EOD:累计 36160 - var rReg = eodSvc.ExecuteCopyEodPosition(initialEod, td, RegDate, StartDate); - AssertDecimalEqual(ExpectedDividend, rReg.PosiDividendSum, 0.01m, "登记日 T日 EOD 累计=36160"); + // T日=4/6(付息日)EOD:累计 36160 + var rPay = eodSvc.ExecuteCopyEodPosition(initialEod, td, PayDate, StartDate); + AssertDecimalEqual(ExpectedDividend, rPay.PosiDividendSum, 0.01m, "付息日 T日 EOD 累计=36160"); - // T+1=4/4 盘中部分平仓(50%):GetPreEodDividendSum 返回【全量】36160(不按比例缩放) + // T+1=4/7 盘中部分平仓(50%):GetPreEodDividendSum 返回全量 36160 var dealSvc = new DealSvcStub( - new List { new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } }, - new List { rReg }); - decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 4)); - AssertDecimalEqual(ExpectedDividend, dividendIn, 0.01m, "部分平仓 T+1:DividendIn 仍为全量 36160(非按 50% 缩放)"); + new List { new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate } }, + new List { rPay }); + decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 7)); + AssertDecimalEqual(ExpectedDividend, dividendIn, 0.01m, "部分平仓 T+1:DividendIn 仍为全量 36160"); - // T+1=4/4 EOD 部分平仓(Quantity=Qty/2):PosiQuantity>0;TdPosiDividend=0(非登记日), + // T+1=4/7 EOD 部分平仓(Quantity=Qty/2):PosiQuantity>0;TdPosiDividend=0(非付息日), // PosiDividendSum = 前日36160 + 0 - TdCloseDividend(全量36160) = 0 - var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rReg, td, new DateTime(2026, 4, 4), RegDate, - new List { CloseEvent(Qty / 2, dividendIn, new DateTime(2026, 4, 4)) }); + var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rPay, td, new DateTime(2026, 4, 7), PayDate, + new List { CloseEvent(Qty / 2, dividendIn, new DateTime(2026, 4, 7)) }); AssertDecimalEqual(ExpectedDividend, rT1.TdCloseDividend, 0.01m, "TdCloseDividend=全量 DividendIn(36160)"); AssertDecimalEqual(0m, rT1.PosiDividendSum, 0.01m, - "部分平仓后剩余 PosiDividendSum=前日36160 - 全量实现36160 = 0(当前生产行为:DividendIn 不按比例缩放)"); + "部分平仓后剩余 PosiDividendSum=前日36160 - 全量实现36160 = 0"); Console.WriteLine($"[部分平仓 T+1] DividendIn={dividendIn}(全量), 剩余PosiDividendSum={rT1.PosiDividendSum}"); } } diff --git a/YLErpDAL/Modules/EodModule/BondPaymentService.cs b/YLErpDAL/Modules/EodModule/BondPaymentService.cs index fe12ab75..fc199e56 100644 --- a/YLErpDAL/Modules/EodModule/BondPaymentService.cs +++ b/YLErpDAL/Modules/EodModule/BondPaymentService.cs @@ -92,19 +92,18 @@ namespace YLErp.Modules.EodModule /// /// 获取某债券的期间付息情况集合 /// - /// + /// /// /// /// public List GetBondPayments(string underlyingCode, DateTime startDate, DateTime endDate) { - // GLMS-20260105-0006:票息归属按债权登记日(reg_date)判定,而非支付日(pay_date_PL/pay_date_act)。 - // 登记日当天 EOD 即应计提;原按支付日口径会让"登记日≠支付日"的债券漏计(二者恰差一工作日时缺陷被掩盖)。 + // 票息归属按实际付息日(payment_date)判定 var result = QueryBondPayments(underlyingCode) - .Where(x => x.reg_date > startDate && x.reg_date <= endDate) + .Where(x => x.payment_date > startDate && x.payment_date <= endDate) .AsNoTracking().ToList(); - Log.Info($"[分红-登记日口径] 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")))); + Log.Info($"[分红-付息日口径] GetBondPayments underlyingCode={underlyingCode} 区间=({startDate:yyyy-MM-dd},{endDate:yyyy-MM-dd}] 按payment_date过滤, 命中 {result.Count} 条: " + + string.Join(",", result.Select(r => r.payment_date?.ToString("yyyy-MM-dd")))); // 让 Copy/Update EOD 始终只依赖 BondPaymentService,而不必在收盘链路直接累加 ex_dividend_info。 // Stock/Fund 公司行为直接从 ex_dividend_info 读取,内部仍按“每 10 份派现金额”保存 GiveCashAmount 原值, @@ -142,7 +141,7 @@ namespace YLErp.Modules.EodModule /// /// 可测性 seam:返回某债券的全部付息记录(未做日期过滤)。测试可 override 注入内存数据, - /// 以验证日期口径(GLMS-20260105-0006:应按债权登记日 reg_date 而非支付日 pay_date_PL/pay_date_act 判定)。 + /// 以验证日期口径(按付息日 payment_date 判定)。 /// protected virtual IQueryable QueryBondPayments(string underlyingCode) => DbContext.bondPayment.Where(x => x.underlyingCode == underlyingCode); @@ -157,7 +156,7 @@ namespace YLErp.Modules.EodModule /// /// 计算某债券某段时间的期间付息 /// - /// 债券代码 + /// 债券代码 /// 计息开始日 /// 计息结束日 /// 持仓数量