Files
zszq-trs/UnitTestProject/Modules/SwapModule/DividendEodNoDoubleCountTest.cs
T
hjhan 37b889c3bf test(dividend): 补全平端到端测试——登记日全平按交易场所规定不享有当日分红
全平场景端到端测试(DividendIn 由 GetPreEodDividendSum 真实算,非喂常量)。
登记日当日全平(盘中平仓→收盘持仓0),按各交易场所规定不享有登记日当日分红(股权登记日以收盘在册为准)。
系统行为正确:①DividendIn 读 T-1(=T日前待实现,正确不含当日);②EOD 全平 PosiQuantity=0 不计提当日。
断言:应得(T日前待实现)==实拿(DividendIn)(不享有当日符合规定)+ EOD 不计提当日。

合并原 d9a55fd1 + 830c72c4:前者误判'丢失缺陷',后者按业务规则修正为'不享有、系统正确'。
2026-08-14 12:49:45 +08:00

227 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.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 端到端:盘中收益互换(DividendIn 由生产方法 GetPreEodDividendSum 真实算出)→ 保存 → EOD,
/// 验证分红【不重复累计】(EOD TdCloseDividend 扣减 DividendIn)且【不丢失】(当日新计进 PosiDividendSum)。
///
/// 与 MultiUnwindDividendConservationTest.MU_001 的区别:MU_001 的互换 DividendIn 是测试喂的常量;
/// 本测试的 DividendIn 由生产方法 GetPreEodDividendSum 真实算出(读 EOD 快照),再喂给 EOD——
/// 覆盖"预览算 DividendIn + EOD 扣减"的完整链路(MU_001 的缺口)。
/// </summary>
[TestClass]
public class DividendEodNoDoubleCountTest
{
private const int SwapTradeId = 9200;
private const long PositionId = 9201;
private const decimal InitialQty = 1000m;
private const decimal DailyRatePerUnit = 0.01m; // 每单位每天 0.01,便于手算
private static readonly DateTime StartDate = new(2026, 1, 5);
#region Stubs
/// <summary>SwapDealService stub:暴露 GetPreEodDividendSum,注入 EOD 数据(不连库)。</summary>
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);
}
/// <summary>SwapEodPositionService stub:暴露 UpdateEodPosition/CopyEodPosition + 线性 CalcBondPayment。</summary>
private sealed class EodSvcStub : TestableSwapEodPositionService
{
public EodSvcStub() : base(nameof(DividendEodNoDoubleCountTest)) { }
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
int days = Math.Max(0, (int)(toDate - fromDate).TotalDays);
return DailyRatePerUnit * days * 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 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);
public eod_swap_position ExecuteCopyEodPosition(eod_swap_position eod, trade td, DateTime valueDate, DateTime preSettleDate)
=> CopyEodPosition(eod, null, td, valueDate, preSettleDate);
}
#endregion
#region 数据构建
private static trade CreateTrade() => new trade
{
id = SwapTradeId, TradeNumber = "UT-DIV-EOD-001", ClientId = 999999,
TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate,
ExerciseDate = new DateTime(2027, 1, 5), TradeStatus = "确认成交", ValidState = "Valid",
StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY",
OriginalStockEqvNotional = (double)(InitialQty * 1.00m)
};
private static swap_position CreatePosition() => new swap_position
{
id = PositionId, SwapTradeId = SwapTradeId,
PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "210210.IB", ContractSize = 1m,
PosiQuantity = InitialQty, PosiNotionalValue = InitialQty,
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 = SwapTradeId, PositionId = PositionId,
ValueDate = StartDate, PosiQuantity = InitialQty,
PosiDirection = (int)SwapDirectionEnum.收取, PositionType = (int)PositionTypeFlag.Long,
UnderlyingCode = "210210.IB", 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 SwapEvent(decimal dividendIn, DateTime eventDate) => new swap_flow_event
{
SwapTradeId = SwapTradeId, EventType = (int)SwapFlowEventTypeEnum.互换,
PositionId = PositionId, Quantity = 0m, DividendIn = dividendIn,
MarkClosePnl = 0m, CloseFee = 0m, TradingFeePending = 0m,
EventDate = eventDate, PayDate = eventDate,
DataState = (int)SwapFlowDateStateEnum.完成
};
private static swap_flow_event CloseEvent(decimal qty, decimal dividendIn, DateTime eventDate) => new swap_flow_event
{
SwapTradeId = SwapTradeId, 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(Math.Abs(expected - actual) <= tol, $"{msg}: expected={expected} actual={actual}");
#endregion
/// <summary>
/// 盘中收益互换:DividendIn 由 GetPreEodDividendSum 真实算(读 T-1 EOD)→ 保存 → EOD。
/// 验证:不重复(EOD TdCloseDividend 扣 DividendIn+ 不丢失(当日新计进 PosiDividendSum+ 守恒。
///
/// 序列(StartDate=1/5,每日 0.01×1000=10):
/// D1=1/6 无事件 CopyPosiDividendSum = 0 + 10 = 10
/// D2=1/7 盘中互换:GetPreEodDividendSum(读 D1) → DividendIn=10;保存 swap_eventEOD:新计 10 - 实现 10 → PosiDividendSum=10
/// 守恒:全程新计(10+10) - 全程实现(10) = 末尾 PosiDividendSum(10)
/// </summary>
[TestMethod]
public void 盘中收益互换_DividendIn真实算_保存后EOD_不重复不丢失()
{
var eodSvc = new EodSvcStub();
var td = CreateTrade();
var position = CreatePosition();
var initialEod = CreateInitialEod();
// D1=1/6 无事件 EOD
var d1 = new DateTime(2026, 1, 6);
var r1 = eodSvc.ExecuteCopyEodPosition(initialEod, td, d1, StartDate);
AssertDecimalEqual(10m, r1.PosiDividendSum, 0.01m, "D1 PosiDividendSum(0+1天×10)");
// D2=1/7 盘中:DividendIn 由生产方法 GetPreEodDividendSum 真实算(读 D1 EOD,当日 EOD 未生成)
var d2 = new DateTime(2026, 1, 7);
var dealSvc = new DealSvcStub(
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = d1 } },
new List<eod_swap_position> { r1 });
decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(SwapTradeId, PositionId, d2);
AssertDecimalEqual(10m, dividendIn, 0.01m, "盘中 DividendIn=GetPreEodDividendSum 读 T-1(D1)=10");
Console.WriteLine($"[盘中预览] DividendIn={dividendIn}(读 T-1 EOD PosiDividendSum={r1.PosiDividendSum}");
// 保存互换事件(DividendIn=真实算出的值,模拟界面点收益互换后保存)
var swapEvent = SwapEvent(dividendIn, d2);
// D2=1/7 EODUpdateEodPosition,真实生产递推)
var r2 = eodSvc.ExecuteUpdateEodPosition(position, r1, td, d2, d1, new List<swap_flow_event> { swapEvent });
// 断言:不重复 + 不丢失
AssertDecimalEqual(10m, r2.TdPosiDividend, 0.01m, "D2 当日新计(1天×10)");
AssertDecimalEqual(dividendIn, r2.TdCloseDividend, 0.01m, "D2 TdCloseDividend=互换DividendIn(扣减→不重复累计)");
AssertDecimalEqual(10m, r2.PosiDividendSum, 0.01m, "D2 PosiDividendSum=前日10+新计10-实现10=10(当日新计挂着→不丢失)");
// 守恒:全程新计 - 全程实现 = 末尾 PosiDividendSum
decimal totalNew = r1.TdPosiDividend + r2.TdPosiDividend;
decimal totalRealized = r2.TdCloseDividend;
AssertDecimalEqual(r2.PosiDividendSum, totalNew - totalRealized, 0.01m,
$"守恒:末尾 PosiDividendSum({r2.PosiDividendSum}) = 全程新计({totalNew}) - 全程实现({totalRealized})");
Console.WriteLine($"[EOD 后] TdPosiDividend={r2.TdPosiDividend} TdCloseDividend={r2.TdCloseDividend} PosiDividendSum={r2.PosiDividendSum}");
Console.WriteLine($"结论:互换实现 {dividendIn} 被扣减(不重复);当日新计 {r2.TdPosiDividend} 挂 PosiDividendSum(不丢失)");
}
/// <summary>
/// 登记日当日全平(盘中平仓→收盘持仓 0):按各交易场所规定,不享有登记日当日的分红
/// (股权登记日以收盘在册为准;盘中全平→收盘不在册)。验证系统行为符合该规定。
///
/// 系统行为:①盘中 DividendIn=GetPreEodDividendSum 读 T-1(=T日前待实现,正确不含登记日当日);
/// ②EOD 全平 PosiQuantity=0 → TdPosiDividend=0(不计提登记日当日)+ PosiDividendSum=0。
/// 即登记日当日分红既不进 DividendIn、也不进 PosiDividendSum = 正确不享有。
/// 应得 = T日前待实现累计(r1.PosiDividendSum);实拿 = DividendIn → 相等,无丢失(不享有当日是正确的)。
/// </summary>
[TestMethod]
public void 登记日全平_按交易场所规定不享有当日分红()
{
var eodSvc = new EodSvcStub();
var td = CreateTrade();
var position = CreatePosition();
var initialEod = CreateInitialEod();
// D1=1/6 无事件 EOD
var d1 = new DateTime(2026, 1, 6);
var r1 = eodSvc.ExecuteCopyEodPosition(initialEod, td, d1, StartDate);
AssertDecimalEqual(10m, r1.PosiDividendSum, 0.01m, "D1 PosiDividendSum");
// D2=1/7 盘中全平:DividendIn 由生产方法真实算(读 D1 EOD,当日 EOD 未生成)
var d2 = new DateTime(2026, 1, 7);
var dealSvc = new DealSvcStub(
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = d1 } },
new List<eod_swap_position> { r1 });
decimal dividendIn = dealSvc.ExposeGetPreEodDividendSum(SwapTradeId, PositionId, d2);
AssertDecimalEqual(10m, dividendIn, 0.01m, "全平 DividendIn=读T-1(D1)=10(漏 D2 当日新计)");
// 全平事件(扣全部持仓)
var closeEvent = CloseEvent(InitialQty, dividendIn, d2);
// D2=1/7 EODUpdateEodPosition,全平→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 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}");
// 断言:实拿 = 应得(登记日全平不享有当日,符合交易场所规定)
AssertDecimalEqual(expectedTotal, actualGot, 0.01m,
$"实拿应=应得(T日前待实现{expectedTotal}),登记日全平不享有当日分红(符合交易场所规定)");
AssertDecimalEqual(0m, r2.TdPosiDividend, 0.01m, "登记日全平 EOD 不计提当日(PosiQuantity=0,正确)");
AssertDecimalEqual(0m, r2.PosiDividendSum, 0.01m, "全平后 PosiDividendSum=0");
}
}
}