fix(bond): #JTZG-136 国联民生-有期间付息,自动互换的的时候互换日期不正确
- 将 BondPaymentService.GetBondPayments 方法的过滤条件从 reg_date 改为 payment_date - 更新相关日志信息显示 [分红-付息日口径] 替代 [分红-登记日口径] - 修正单元测试中的注释和变量名,统一使用 payment_date 口径描述 - 修改测试数据构造,验证按实际付息日进行票息计提的逻辑 - 更新相关文档注释,明确日期口径变更的具体实现方式
This commit is contained in:
@@ -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<BondPayment> BondPayments() => new List<BondPayment>
|
||||
{
|
||||
// 登记日 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);
|
||||
}
|
||||
|
||||
/// <summary>真实 BondPaymentService(reg_date 口径)seam:仅注入内存 BondPayment 数据,票息计算走生产 GetBondPayments+CalcPayment。</summary>
|
||||
/// <summary>真实 BondPaymentService(payment_date 口径)seam:仅注入内存 BondPayment 数据,票息计算走生产 GetBondPayments+CalcPayment。</summary>
|
||||
private sealed class RealBondPaymentService : BondPaymentService
|
||||
{
|
||||
private readonly List<BondPayment> _data;
|
||||
@@ -60,14 +60,14 @@ namespace YLErp.Modules.SwapModule
|
||||
=> _data.Where(x => x.underlyingCode == underlyingCode).AsQueryable();
|
||||
}
|
||||
|
||||
/// <summary>SwapEodPositionService stub:暴露 UpdateEodPosition/CopyEodPosition;CalcBondPayment 桥接真实 BondPaymentService(reg_date 口径,不再用线性假公式)。</summary>
|
||||
/// <summary>SwapEodPositionService stub:暴露 UpdateEodPosition/CopyEodPosition;CalcBondPayment 桥接真实 BondPaymentService(payment_date 口径)。</summary>
|
||||
private sealed class EodSvcStub : TestableSwapEodPositionService
|
||||
{
|
||||
private readonly List<BondPayment> _bondPayments;
|
||||
public EodSvcStub(List<BondPayment> 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)
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
@@ -196,16 +196,10 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登记日当日全平(盘中平仓→收盘持仓 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):按各交易场所规定,不享有付息日当日的分红。
|
||||
/// </summary>
|
||||
[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<swap_flow_event> { 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【死代码删除的边界规格】脏数据(OriginalStockEqvNotional=null / PosiNetPrice=0)不得让
|
||||
/// UpdateEodPosition 崩溃,且分红产出与正常数据完全一致。
|
||||
/// 背景:这两个字段在 UpdateEodPosition 内的唯一消费点是历史遗留死代码
|
||||
/// (originNotional→totalPayment 全历史重算,结果从未被使用,2026-08 论证后删除)——
|
||||
/// 删除前该脏数据会在 EOD 抛 InvalidOperationException/除零;删除后是设计内行为。
|
||||
/// 本测试同时钉住:删除后输出等价(与同输入正常数据路径一致)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void 脏数据边界_死代码涉及字段_不影响EOD分红产出()
|
||||
|
||||
Reference in New Issue
Block a user