using YLErp.Modules.EodModule;
namespace YLErp.Modules.SwapModule
{
///
/// 债券 TRS 付息日(payment_date)分红测试。
/// 验证:
/// A. BondPaymentService.GetBondPayments 用实际付息日(payment_date)判定付息归属
/// B. SwapDealService.GetPreEodDividendSum 读 EOD 快照
///
[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 PrePayDate = new(2026, 4, 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)
};
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 _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/5, 4/6]
var payments = svc.GetBondPayments(BondCode, PrePayDate, PayDate);
// 按支付日(payment_date=4/6)过滤 -> 4/6 落在区间 -> 1 条
Assert.AreEqual(1, payments.Count,
"付息日(4/6)当日 EOD 应按实际付息日(payment_date)选中该笔付息。");
}
[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 条(验证按 payment_date 过滤)
for (int i = 0; i < 5; i++)
{
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, PayDates[0].AddDays(-1), PayDates[4]);
Assert.AreEqual(5, all.Count, "长区间(付息日1前,付息日5] 应命中全部 5 次付息");
// 跨付息日中间区间:(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, 6), new DateTime(2026, 4, 30) },
mid.Select(x => x.payment_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, 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 期");
}
#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/5 EOD:累计分红 0;4/6 EOD(付息日):累计分红 36160
var eodSwaps = new List
{
new eod_swap { SwapTradeId = TradeId, ValueDate = PrePayDate },
new eod_swap { SwapTradeId = TradeId, ValueDate = PayDate }
};
var eodPositions = new List
{
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/6)当天手动平仓
var dividend = svc.ExposeGetPreEodDividendSum(TradeId, PositionId, PayDate);
Assert.AreEqual(ExpectedDividend, dividend, 0.01m,
"付息日(4/6)当天手动平仓应读到当日 EOD 累计分红 36,160。");
}
[TestMethod]
public void CauseB_MultiRegDate_Auto实现归0后下次付息日重新累加()
{
// 模拟:付息日1(3/2)计提 36160 → auto互换实现归0(3/3) → 付息日2(4/6)再计提 36160
var eodSwaps = new List
{
new eod_swap { SwapTradeId = TradeId, ValueDate = new DateTime(2026,3,1) },
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
{
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,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/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/6)手动互换应读当日EOD=第二次分红36160。");
}
[TestMethod]
public void CauseB_MultiRegDate_手动互换期间分红挂账累计四期()
{
// 模拟:多次付息日之间未 auto 实现,分红挂账累加
// 4/6=36160, 4/30=72320, 6/1=108480, 6/30=144640(4期累计)
var eodSwaps = new List
{
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
{
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, 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
}
}