fix(bond): #JTZG-136 国联民生-有期间付息,自动互换的的时候互换日期不正确
- 将 BondPaymentService.GetBondPayments 方法的过滤条件从 reg_date 改为 payment_date - 更新相关日志信息显示 [分红-付息日口径] 替代 [分红-登记日口径] - 修正单元测试中的注释和变量名,统一使用 payment_date 口径描述 - 修改测试数据构造,验证按实际付息日进行票息计提的逻辑 - 更新相关文档注释,明确日期口径变更的具体实现方式
This commit is contained in:
@@ -3,13 +3,10 @@ using YLErp.Modules.EodModule;
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 快照
|
||||
/// </summary>
|
||||
[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<BondPayment> { 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<eod_swap>
|
||||
{
|
||||
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<eod_swap_position>
|
||||
{
|
||||
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<eod_swap>
|
||||
{
|
||||
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<eod_swap_position>
|
||||
{
|
||||
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<eod_swap>
|
||||
{
|
||||
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<eod_swap_position>
|
||||
{
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user