fix(bond): #JTZG-136 国联民生-有期间付息,自动互换的的时候互换日期不正确

- 将 BondPaymentService.GetBondPayments 方法的过滤条件从 reg_date 改为 payment_date
- 更新相关日志信息显示 [分红-付息日口径] 替代 [分红-登记日口径]
- 修正单元测试中的注释和变量名,统一使用 payment_date 口径描述
- 修改测试数据构造,验证按实际付息日进行票息计提的逻辑
- 更新相关文档注释,明确日期口径变更的具体实现方式
This commit is contained in:
张名锐
2026-08-26 13:40:59 +08:00
parent 1c879c0b65
commit ac7b04e79d
4 changed files with 143 additions and 185 deletions
@@ -8,16 +8,7 @@ using System.Linq;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// GLMS-20260105-0006 端到端补充:EOD 分红引擎票息归属按【债权登记日 reg_date】判定
/// 而非支付日(pay_date)。此前 DividendEodNoDoubleCountTest.EodSvcStub 把 CalcBondPayment 覆写成
/// 线性公式(DailyRatePerUnit*days*qty)**绕开了 reg_date 口径**——即没有真正验证"引擎按登记日计提"。
///
/// 本文件把 EOD stub 的 CalcBondPayment seam 重新桥接回【真实的 BondPaymentServicereg_date 口径)】,
/// 仅用内存 BondPayment 数据(不连库),使端到端流程(CopyEodPosition/UpdateEodPosition + GetPreEodDividendSum)
/// 真正跑生产日期逻辑:
/// ① EOD 引擎在登记日计提、支付日不计提(证明 reg_date 口径);
/// ② 登记日下一日(T+1)全平:经 GetPreEodDividendSum 读到登记日当日 EOD 分红(收盘在册→享有);
/// ③ 部分平仓 T+1DividendIn 为全量(非按比例缩放),剩余 PosiDividendSum 归 0(记录当前生产行为)。
/// EOD 分红引擎票息归属按【实际付息日 payment_date】判定测试。
/// </summary>
[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<BondPayment> BondPayments()
=> new List<BondPayment>
@@ -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
/// <summary>
/// 端到端证 reg_date 口径:EOD 引擎(CopyEodPosition)逐日计提时,
/// 仅在【债权登记日】产生分红,【支付日】不产生(即便支付日与登记日相差数日)
/// 这是线性 stub 无法覆盖的——线性公式按"天数"算,永远无法区分登记日 vs 支付日。
/// 端到端证 payment_date 口径:EOD 引擎(CopyEodPosition)逐日计提时,
/// 仅在【实际付息日】产生分红,【登记日/非付息日】不产生
/// </summary>
[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_date4/3 不>4/34/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}(付息日计提)");
}
/// <summary>
/// 用户场景「登记日下一日(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。
/// </summary>
[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:引擎算出分红 36160reg_date 口径)
var rReg = eodSvc.ExecuteCopyEodPosition(initialEod, td, RegDate, StartDate);
AssertDecimalEqual(ExpectedDividend, rReg.PosiDividendSum, 0.01m, "登记日 T日 EOD 累计分红=36160");
// T日=4/6(付息日)EOD:引擎算出分红 36160payment_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<eod_swap> { new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } },
new List<eod_swap_position> { rReg });
decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 4));
new List<eod_swap> { new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate } },
new List<eod_swap_position> { 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<swap_flow_event> { 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<swap_flow_event> { 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}");
}
/// <summary>
/// 部分平仓 T+1当前生产行为记录(非修复目标)。
/// T日(登记日)持有→T+1盘中部分平仓:GetPreEodDividendSum 返回的是全量待实现分红(非按平仓比例缩放)
/// 故 DividendIn=全量 36160T+1 EOD 部分平仓(PosiQuantity>0)后剩余 PosiDividendSum=前日-全量=0。
/// 注:此"DividendIn 不按平仓比例缩放"是当前生产行为,已与用户确认(潜在一致性议题,非本 bug 修复范围)。
/// 部分平仓 T+1
/// T日(付息日)持有→T+1盘中部分平仓:GetPreEodDividendSum 返回的是全量待实现分红,
/// 故 DividendIn=全量 36160T+1 EOD 部分平仓后剩余 PosiDividendSum=前日-全量=0。
/// </summary>
[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<eod_swap> { new eod_swap { SwapTradeId = TradeId, ValueDate = RegDate } },
new List<eod_swap_position> { rReg });
decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 4));
AssertDecimalEqual(ExpectedDividend, dividendIn, 0.01m, "部分平仓 T+1DividendIn 仍为全量 36160(非按 50% 缩放)");
new List<eod_swap> { new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate } },
new List<eod_swap_position> { rPay });
decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(TradeId, PositionId, new DateTime(2026, 4, 7));
AssertDecimalEqual(ExpectedDividend, dividendIn, 0.01m, "部分平仓 T+1DividendIn 仍为全量 36160");
// T+1=4/4 EOD 部分平仓(Quantity=Qty/2)PosiQuantity>0TdPosiDividend=0(非登记日)
// T+1=4/7 EOD 部分平仓(Quantity=Qty/2)PosiQuantity>0TdPosiDividend=0(非付息日)
// PosiDividendSum = 前日36160 + 0 - TdCloseDividend(全量36160) = 0
var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rReg, td, new DateTime(2026, 4, 4), RegDate,
new List<swap_flow_event> { CloseEvent(Qty / 2, dividendIn, new DateTime(2026, 4, 4)) });
var rT1 = eodSvc.ExecuteUpdateEodPosition(position, rPay, td, new DateTime(2026, 4, 7), PayDate,
new List<swap_flow_event> { 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}");
}
}