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 注入,真实跑生产日期逻辑。 /// [TestClass] public class GLMS20260105_0006_RegisterDateDividendTest { private const string BondCode = "230004.IB"; private const int TradeId = 6006; private const long PositionId = 60061; private const decimal Qty = 20_000_000m; private const decimal PaymentPer100 = 0.1808m; private const decimal ExpectedDividend = 36_160m; // 20,000,000 × 0.1808 / 100 // 付息日历(截图):登记日 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); #region 成因 A:日期口径 seam private sealed class TestableBondPaymentService : BondPaymentService { private readonly List _data; public TestableBondPaymentService(List data) : base(OptUserInfo.UnitTestUser) { _data = data; } protected override IQueryable QueryBondPayments(string underlyingCode) => _data.Where(x => x.underlyingCode == underlyingCode).AsQueryable(); } [TestMethod] public void CauseA_登记日当日EOD_应按登记日口径选中付息记录() { var record = new BondPayment { underlyingCode = BondCode, 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); // 修复前:用支付日(pay_date_PL=4/6)过滤 -> 4/6 不在 (4/2,4/3] -> 0 条(漏计分红) // 修复后:用债权登记日(reg_date=4/3)过滤 -> 4/3 落在区间 -> 1 条(GLMS-20260105-0006 已修复) Assert.AreEqual(1, payments.Count, "登记日(4/3)当日 EOD 应按债权登记日(reg_date)选中该笔付息;" + "当前按支付日(pay_date_PL=4/6)过滤会漏选->0条,导致分红不计提。"); } #endregion #region 成因 B:T-1 快照 seam private sealed class TestableSwapDealService : SwapDealService { private readonly List _eodSwaps; private readonly List _eodPositions; public TestableSwapDealService(List eodSwaps, List eodPositions) : base(OptUserInfo.UnitTestUser) { _eodSwaps = eodSwaps; _eodPositions = eodPositions; } public decimal ExposeGetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate) => GetPreEodDividendSum(tradeId, positionId, dealDate); protected override IQueryable QueryPreEodSwaps(int tradeId) => _eodSwaps.Where(x => x.SwapTradeId == tradeId).AsQueryable(); protected override eod_swap_position QueryPreEodPosition(int tradeId, long positionId, DateTime valueDate) => _eodPositions.FirstOrDefault(x => x.SwapTradeId == tradeId && x.PositionId == positionId && x.ValueDate == valueDate); } [TestMethod] public void CauseB_登记日当天手动平仓_应读到当日EOD分红36160() { // 4/2 EOD:累计分红 0;4/3 EOD(登记日):累计分红 36160(即登记日应有的状态) var eodSwaps = new List { new eod_swap { SwapTradeId = TradeId, ValueDate = PreRegDate }, new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } }; 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 } }; var svc = new TestableSwapDealService(eodSwaps, eodPositions); // 登记日(4/3)当天手动平仓 var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, RegDate); // 修复前: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。"); } #endregion } }