Files
zszq-trs/UnitTestProject/Modules/SwapModule/RegDateDividendEodE2ETest.cs
T
张名锐 ac7b04e79d fix(bond): #JTZG-136 国联民生-有期间付息,自动互换的的时候互换日期不正确
- 将 BondPaymentService.GetBondPayments 方法的过滤条件从 reg_date 改为 payment_date
- 更新相关日志信息显示 [分红-付息日口径] 替代 [分红-登记日口径]
- 修正单元测试中的注释和变量名,统一使用 payment_date 口径描述
- 修改测试数据构造,验证按实际付息日进行票息计提的逻辑
- 更新相关文档注释,明确日期口径变更的具体实现方式
2026-08-26 13:40:59 +08:00

255 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;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Modules.EodModule;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// EOD 分红引擎票息归属按【实际付息日 payment_date】判定测试。
/// </summary>
[TestClass]
public class RegDateDividendEodE2ETest
{
private const string BondCode = "230004.IB";
private const int TradeId = 7004;
private const long PositionId = 70041;
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
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); // 实际支付日
#region 内存债券付息数据(payment_date 口径)
private static List<BondPayment> BondPayments()
=> new List<BondPayment>
{
new BondPayment
{
underlyingCode = BondCode,
reg_date = RegDate,
payment_date_pl = PayDate,
payment_date = PayDate, // 实际付息日
payment_interest = PaymentPer100
}
};
#endregion
#region BondPaymentService seam(桥接真实 payment_date 口径,内存数据)
private sealed class RegDateBondPaymentService : BondPaymentService
{
private readonly List<BondPayment> _data;
public RegDateBondPaymentService(List<BondPayment> data, OptUserInfo userInfo) : base(userInfo) { _data = data; }
protected override IQueryable<BondPayment> QueryBondPayments(string underlyingCode)
=> _data.Where(x => x.underlyingCode == underlyingCode).AsQueryable();
}
#endregion
#region EOD stubCalcBondPayment 桥接真实 BondPaymentService
private sealed class RegDateEodStub : TestableSwapEodPositionService
{
private readonly List<BondPayment> _bondPayments;
public RegDateEodStub(List<BondPayment> bondPayments) : base(nameof(RegDateDividendEodE2ETest)) { _bondPayments = bondPayments; }
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
// 桥接真实生产口径:BondPaymentService.GetBondPayments 按 payment_date 过滤 + CalcPayment 累加
var svc = new RegDateBondPaymentService(_bondPayments, OptUserInfo.UnitTestUser);
return svc.CalcPayment(underlyingCode, fromDate, toDate, qty, shortRatio, directionRatio);
}
protected override underlying_manager GetUnderlyingData(string underlyingCode)
=> new underlying_manager { ValueAddedTax = 0m };
protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{ vobp = 0m; return 1.00m; }
public eod_swap_position ExecuteCopyEodPosition(eod_swap_position eod, trade td, DateTime valueDate, DateTime preSettleDate)
=> CopyEodPosition(eod, null, td, valueDate, preSettleDate);
public eod_swap_position ExecuteUpdateEodPosition(swap_position swapPosition, eod_swap_position eod, trade td, DateTime valueDate, DateTime preSettleDate, List<swap_flow_event> unwindEvents)
=> UpdateEodPosition(swapPosition, eod, null, td, valueDate, preSettleDate, unwindEvents);
}
#endregion
#region Deal stubGetPreEodDividendSum,注入 EOD 快照)
private sealed class DealSvcStub : SwapDealService
{
private readonly List<eod_swap> _eodSwaps;
private readonly List<eod_swap_position> _eodPositions;
public DealSvcStub(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);
}
#endregion
#region 数据构建
private static trade CreateTrade() => new trade
{
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",
OriginalStockEqvNotional = (double)(Qty * 1.00m)
};
private static swap_position CreatePosition() => new swap_position
{
id = PositionId, SwapTradeId = TradeId,
PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = BondCode, ContractSize = 1m,
PosiQuantity = Qty, PosiNotionalValue = Qty,
PosiNetPrice = 1.000m, PosiGrossPrice = 1.000m,
PosiNetFeePrice = 1.000m, PosiNetNoFeePrice = 1.000m,
IsInitial = true, Invalid = false,
PosiTradingFee = 0, PosiTradingFeePending = 0
};
private static eod_swap_position CreateInitialEod() => new eod_swap_position
{
id = 1, SwapTradeId = TradeId, PositionId = PositionId,
ValueDate = StartDate, PosiQuantity = Qty,
PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = BondCode, ContractSize = 1m,
PosiNetPrice = 1.000m, PosiGrossPrice = 1.000m,
PosiNetFeePrice = 1.000m, PosiNetNoFeePrice = 1.000m,
PosiDividendSum = 0m, TdPosiDividend = 0m, TdCloseDividend = 0m,
RealizedDividend = 0m, PosiFeePending = 0m,
InterestProfitSum = 0m, Invalid = false
};
private static swap_flow_event CloseEvent(decimal qty, decimal dividendIn, DateTime eventDate) => new swap_flow_event
{
SwapTradeId = TradeId, EventType = (int)SwapFlowEventTypeEnum.平仓,
PositionId = PositionId, Quantity = qty, DividendIn = dividendIn,
MarkClosePnl = 0m, CloseFee = 0m, TradingFeePending = 0m,
TradingAmount = qty * 1.000m,
UnwindDate = eventDate, EventDate = eventDate, PayDate = eventDate,
DataState = (int)SwapFlowDateStateEnum.完成
};
private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tol, string msg)
=> Assert.IsTrue(System.Math.Abs(expected - actual) <= tol, $"{msg}: expected={expected} actual={actual}");
#endregion
/// <summary>
/// 端到端证 payment_date 口径:EOD 引擎(CopyEodPosition)逐日计提时,
/// 仅在【实际付息日】产生分红,【登记日/非付息日】不产生。
/// </summary>
[TestMethod]
public void 付息日口径_EOD引擎按payment_date计提()
{
var eodSvc = new RegDateEodStub(BondPayments());
var td = CreateTrade();
var initialEod = CreateInitialEod();
// 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 累计(无付息日)");
// D2=4/3(登记日,非付息日):窗口 (4/2,4/3] 无付息日 → 0
var r2 = eodSvc.ExecuteCopyEodPosition(r1, td, RegDate, StartDate);
AssertDecimalEqual(0m, r2.TdPosiDividend, 0.01m, "4/3 登记日(非付息日)不计提");
AssertDecimalEqual(0m, r2.PosiDividendSum, 0.01m, "4/3 累计=0");
// D3=4/6(付息日):窗口 (4/3,4/6] 命中 payment_date=4/6 → 36160
var r3 = eodSvc.ExecuteCopyEodPosition(r2, td, PayDate, StartDate);
AssertDecimalEqual(ExpectedDividend, r3.TdPosiDividend, 0.01m, "4/6 付息日当日应计提 36160");
AssertDecimalEqual(ExpectedDividend, r3.PosiDividendSum, 0.01m, "4/6 累计为 36160");
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。
/// </summary>
[TestMethod]
public void 付息日下一日全平_经GetPreEodDividendSum读到付息日分红()
{
var eodSvc = new RegDateEodStub(BondPayments());
var td = CreateTrade();
var position = CreatePosition();
var initialEod = CreateInitialEod();
// 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/7 盘中:注入 T日 EOD 快照,GetPreEodDividendSum 应读 T日(<=当日) → 36160
var dealSvc = new DealSvcStub(
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/7) 盘中全平应经 GetPreEodDividendSum 读到 T日(4/6)EOD 分红 36160");
Console.WriteLine($"[T+1 全平] DividendIn(读T日EOD)={dividendIn}");
// 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)) });
decimal actualGot = dividendIn + rT1.PosiDividendSum;
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 部分平仓后剩余 PosiDividendSum=前日-全量=0。
/// </summary>
[TestMethod]
public void 部分平仓_T1_DividendIn为全量_剩余PosiDividendSum归0()
{
var eodSvc = new RegDateEodStub(BondPayments());
var td = CreateTrade();
var position = CreatePosition();
var initialEod = CreateInitialEod();
// 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/7 盘中部分平仓(50%)GetPreEodDividendSum 返回全量 36160
var dealSvc = new DealSvcStub(
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/7 EOD 部分平仓(Quantity=Qty/2)PosiQuantity>0TdPosiDividend=0(非付息日)
// PosiDividendSum = 前日36160 + 0 - TdCloseDividend(全量36160) = 0
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");
Console.WriteLine($"[部分平仓 T+1] DividendIn={dividendIn}(全量), 剩余PosiDividendSum={rT1.PosiDividendSum}");
}
}
}