Files
zszq-trs/UnitTestProject/Modules/SwapModule/GLMS20260105_0006_RegisterDateDividendTest.cs
T
hjhan bdba5fbecd fix(EQD-7004): 补全分红登记日口径修复的多次付息累计测试与Trace日志
原修复 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。
2026-08-14 10:05:01 +08:00

236 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 注入,真实跑生产日期逻辑。
/// </summary>
[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);
// 多次付息日历(截图:债券 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
{
private readonly List<BondPayment> _data;
public TestableBondPaymentService(List<BondPayment> data) : base(OptUserInfo.UnitTestUser) { _data = data; }
protected override IQueryable<BondPayment> 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<BondPayment> { 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条,导致分红不计提。");
}
[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 成因 BT-1 快照 seam
private sealed class TestableSwapDealService : SwapDealService
{
private readonly List<eod_swap> _eodSwaps;
private readonly List<eod_swap_position> _eodPositions;
public TestableSwapDealService(List<eod_swap> eodSwaps, List<eod_swap_position> eodPositions)
: base(OptUserInfo.UnitTestUser) { _eodSwaps = eodSwaps; _eodPositions = eodPositions; }
public decimal ExposeGetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
=> GetPreEodDividendSum(tradeId, positionId, dealDate);
protected override IQueryable<eod_swap> 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<eod_swap>
{
new eod_swap { SwapTradeId = TradeId, ValueDate = PreRegDate },
new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate }
};
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 }
};
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) -> 36160GLMS-20260105-0006 已修复)
Assert.AreEqual(ExpectedDividend, dividend, 0.01m,
"登记日(4/3)当天手动平仓应读到当日 EOD 累计分红 36,160" +
"当前 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<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) },
};
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 },
};
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=1446404期累计)
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) },
};
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 },
};
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 应读 723202期累计)");
Assert.AreEqual(3 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 5, 29)), 0.01m, "5/29 应读 1084803期累计)");
// 关键:第 4 期登记日累计 = 4 × 36160 = 144640(原 9df39491 仅覆盖单期 36160,未验证多次付息累计)
Assert.AreEqual(4 * ExpectedDividend, svc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 6, 29)), 0.01m,
"6/29 应读 1446404期累计);原 9df39491 仅覆盖单期 36160,未验证多次付息累计。");
}
#endregion
}
}