全平场景端到端测试(DividendIn 由 GetPreEodDividendSum 真实算,非喂常量)。
登记日当日全平(盘中平仓→收盘持仓0),按各交易场所规定不享有登记日当日分红(股权登记日以收盘在册为准)。
系统行为正确:①DividendIn 读 T-1(=T日前待实现,正确不含当日);②EOD 全平 PosiQuantity=0 不计提当日。
断言:应得(T日前待实现)==实拿(DividendIn)(不享有当日符合规定)+ EOD 不计提当日。
合并原 d9a55fd1 + 830c72c4:前者误判'丢失缺陷',后者按业务规则修正为'不享有、系统正确'。
227 lines
14 KiB
C#
227 lines
14 KiB
C#
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 无事件 Copy:PosiDividendSum = 0 + 10 = 10
|
||
/// D2=1/7 盘中互换:GetPreEodDividendSum(读 D1) → DividendIn=10;保存 swap_event;EOD:新计 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 EOD(UpdateEodPosition,真实生产递推)
|
||
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 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 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");
|
||
}
|
||
}
|
||
}
|