- 在多个单元测试文件中添加InterestIncomeSum字段以保持数据一致性 - 修改GLMS20260105PartialCloseDividendBugTest测试,改进EOD快照验证逻辑 - 修复SwapDealService中预付金腿的orginPv计算逻辑,使用上一日保证金本金作为基准 - 更新SwapPositionComposeScenarioTest中的测试数据结构和利率设置 - 修正平仓日利息计算精度问题,使用Math.Round确保计算准确性
223 lines
14 KiB
C#
223 lines
14 KiB
C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||
using YLErp.DBModels;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
/// <summary>
|
||
/// 【绿灯验收】国联民生-期间结算事件验证-分红前部分平仓过的交易,平仓与分红结束并收盘后,
|
||
/// 继续平仓 / 互换时"浮动端平仓盈亏·分红(DividendIn)"应为 0(修复方案C:后端读 EOD 单一可信源)。
|
||
/// ============================================================================
|
||
/// 参考交易:GLMS-20260105-0004(测试库 id=1921,标的 230004.IB)
|
||
/// 场景时间线(库内真实数据):
|
||
/// 2026-01-05 开仓,期初持仓 50,000,000(支付方向 Direction=2,多头 PositionType=1)
|
||
/// 2026-02-28 部分平仓 40%(20,000,000),剩余 30,000,000;该日早于 3/2 登记日,
|
||
/// 已平仓的 40% 不享有 3/2 付息 → 平仓事件 DividendIn=0(正确)
|
||
/// 2026-03-02 债券期间付息登记日;当日自动互换(EventType=4),按【剩余持仓 30,000,000】
|
||
/// 实现分红 DividendIn=-54,240(=30,000,000 × 0.001808);EOD PosiDividendSum=0(全实现)
|
||
/// 2026-03-03 收盘后对该交易继续平仓/互换
|
||
///
|
||
/// 修复前(Bug):前端 getDivindIn 用期初全额持仓(50M)×totalInterest 算出 -90,400,
|
||
/// 扣除 consumedDividend(-54,240) 得 remainDividend=-36,160 展示 → 错误。
|
||
/// -36,160 恰=已平仓40%(20M)×单位付息(0.001808)×支付方向(-1),即把"登记日前已平仓、
|
||
/// 不享有该笔分红"的部分重复计入。
|
||
///
|
||
/// 修复后(方案C):后端 InitUnwind/InitIncome 经 GetPreEodDividendSum 读上一收盘日
|
||
/// eod_swap_position.PosiDividendSum。3/2 互换后该值=0 → DividendIn=0(正确)。
|
||
/// 前端 getDivindIn 不再自算,直接用后端值。
|
||
/// ============================================================================
|
||
/// 本测试连真实测试库,调用真实 SwapDealService.GetPreEodDividendSum 验证修复后 DividendIn=0。
|
||
/// 连不上库时 Inconclusive 跳过(CI 无 DB 环境不挡)。
|
||
/// </summary>
|
||
[TestClass]
|
||
public class GLMS20260105PartialCloseDividendBugTest
|
||
{
|
||
private const string TradeNumber = "GLMS-20260105-0004";
|
||
// 3/3 继续平仓/互换的业务日期;上一收盘日为 3/2(EOD 已生成,PosiDividendSum=0)
|
||
private static readonly DateTime DealDate0303 = new(2026, 3, 3);
|
||
|
||
/// <summary>
|
||
/// 连真实库的 SwapDealService 子类,仅用于暴露 protected GetPreEodDividendSum 供单测调用。
|
||
/// 不 override 任何 seam → DbContext 走真实 YLContext(与 DbContextFactory.GetYLDbContext() 同库)。
|
||
/// </summary>
|
||
private sealed class RealDbSwapDealService : SwapDealService
|
||
{
|
||
public RealDbSwapDealService() : base(OptUserInfo.UnitTestUser) { }
|
||
|
||
public decimal ExposeGetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||
=> GetPreEodDividendSum(tradeId, positionId, dealDate);
|
||
}
|
||
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Green_3_3_Unwind_DividendIn_FromEOD_Equals_Zero()
|
||
{
|
||
YLContext db;
|
||
try { db = DbContextFactory.GetYLDbContext(); }
|
||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||
|
||
try
|
||
{
|
||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||
|
||
// 浮动腿持仓:非初始、UnderlyingCode 非空(当前剩余 30,000,000)
|
||
var positions = db.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid).ToList();
|
||
var floatLeg = positions.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial);
|
||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓(非初始、UnderlyingCode 非空)");
|
||
|
||
// ---- 调用真实 GetPreEodDividendSum(方案C 修复核心)----
|
||
var service = new RealDbSwapDealService();
|
||
decimal dividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, DealDate0303);
|
||
|
||
// ---- 绿灯断言:3/2 互换后 EOD PosiDividendSum=0 → DividendIn=0 ----
|
||
Assert.AreEqual(0m, dividendIn, 0.01m,
|
||
$"[绿灯·方案C验收] 3/3平仓/互换 DividendIn 期望=0(3/2互换后剩余持仓30M的分红已全部实现," +
|
||
$"EOD PosiDividendSum=0)。实际={dividendIn}。修复前该值为 -36,160(期初持仓×totalInterest 重算误计入已平仓40%)。");
|
||
|
||
// ---- 健全性:上一 EOD 确为 3/2,且 PosiDividendSum=0 ----
|
||
var lastEod = db.eod_swap_position
|
||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && x.ValueDate < DealDate0303 && x.PositionId == floatLeg.PositionId)
|
||
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||
var expectedEodDate = new DateTime(2026, 3, 2);
|
||
if (lastEod?.ValueDate != expectedEodDate)
|
||
{
|
||
Assert.Inconclusive($"测试库未准备 3/2 EOD 快照,当前上一收盘日为 {lastEod?.ValueDate:yyyy-MM-dd}");
|
||
}
|
||
Assert.AreEqual(0m, lastEod.PosiDividendSum, 0.01m,
|
||
$"3/2 EOD PosiDividendSum 应=0(当日 TdPosiDividend={lastEod.TdPosiDividend} 全额由互换 TdCloseDividend={lastEod.TdCloseDividend} 实现)");
|
||
Assert.AreEqual(30_000_000m, lastEod.PosiQuantity, "3/2 剩余持仓应为 30,000,000(2/28已平仓40%)");
|
||
}
|
||
finally { db?.Dispose(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 互换页(InitIncome)与平仓页(InitUnwind)读同一 EOD:同 position、同 dealDate,
|
||
/// GetPreEodDividendSum 返回值必然一致。本测试明确覆盖"点击收益互换同理"路径
|
||
/// (用户反馈:平仓页 -36,160,点击收益互换同样错误)。
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Green_3_3_Income_DividendIn_FromEOD_Equals_Zero()
|
||
{
|
||
YLContext db;
|
||
try { db = DbContextFactory.GetYLDbContext(); }
|
||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||
|
||
try
|
||
{
|
||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||
|
||
var floatLeg = db.swap_position
|
||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||
.FirstOrDefault();
|
||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓");
|
||
|
||
// InitIncome 与 InitUnwind 调用 GetPreEodDividendSum 的入参一致 → 结果一致
|
||
var service = new RealDbSwapDealService();
|
||
decimal incomeDividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, DealDate0303);
|
||
|
||
Assert.AreEqual(0m, incomeDividendIn, 0.01m,
|
||
$"[互换页·方案C验收] 3/3收益互换 DividendIn 期望=0,实际={incomeDividendIn}。" +
|
||
$"InitIncome 与 InitUnwind 共用 GetPreEodDividendSum,应返回同一 EOD 值。");
|
||
}
|
||
finally { db?.Dispose(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 防御性:dealDate 早于任何 EOD(如交易首日尚未收盘)时,GetPreEodDividendSum 应返回 0,
|
||
/// 不抛异常。覆盖"无 EOD 记录"边界——fallback 到 dealDate.AddDays(-1) 后查无数据 → 0。
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Green_NoEodBeforeDealDate_Returns_Zero_WithoutThrowing()
|
||
{
|
||
YLContext db;
|
||
try { db = DbContextFactory.GetYLDbContext(); }
|
||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||
|
||
try
|
||
{
|
||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||
|
||
var floatLeg = db.swap_position
|
||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||
.FirstOrDefault();
|
||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓");
|
||
|
||
var service = new RealDbSwapDealService();
|
||
// dealDate = 交易开始日前一天,确保无任何 EOD 满足 ValueDate < dealDate
|
||
var preStartDate = td.StartDate.Value.AddDays(-1);
|
||
decimal dividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, preStartDate);
|
||
|
||
Assert.AreEqual(0m, dividendIn, 0.01m,
|
||
$"[防御] dealDate({preStartDate:yyyy-MM-dd})早于所有 EOD 时,GetPreEodDividendSum 应返回 0,实际={dividendIn}。" +
|
||
$"与历史 InitUnwind/InitIncome 中 DividendIn=0 行为一致,不抛异常。");
|
||
}
|
||
finally { db?.Dispose(); }
|
||
}
|
||
|
||
// ============================================================================
|
||
// 非0场景验收(关键防回归)
|
||
// ----------------------------------------------------------------------------
|
||
// GLMS-20260105-0004 的 PosiDividendSum 恰好=0(3/2 已全额互换),无法暴露
|
||
// "DividendPending 硬编码 0" 的回归。本测试用一笔 PosiDividendSum≠0 的活交易
|
||
// GLMS-20260706-0004(标的 230004.IB,持仓 1亿,待实现分红 -150,000,多日稳定)
|
||
// 验证:GetPreEodDividendSum 返回非0的 -150,000 → 修复后 DividendIn/DividendPending
|
||
// 均应为此值(而非历史硬0回归)。
|
||
// 若本测试因硬0回归而失败(实际=0),即说明有人把 DividendPending 改回了硬0,
|
||
// 或 GetPreEodDividendSum 传导链路被破坏。
|
||
// ============================================================================
|
||
|
||
/// <summary>
|
||
/// 非0场景:PosiDividendSum≠0 时,GetPreEodDividendSum 返回真实非0值,
|
||
/// 该值应同时成为 DividendIn 与 DividendPending(全量口径)。
|
||
/// 这是针对"硬0被0掩盖"盲区的核心防回归测试。
|
||
/// </summary>
|
||
[TestMethod]
|
||
[TestCategory("DBRecording")]
|
||
public void Green_NonZeroPosiDividendSum_Returns_RealValue_NotHardZero()
|
||
{
|
||
const string nonZeroTrade = "GLMS-20260706-0004"; // PosiDividendSum=-150,000,确认成交活交易
|
||
YLContext db;
|
||
try { db = DbContextFactory.GetYLDbContext(); }
|
||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||
|
||
try
|
||
{
|
||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == nonZeroTrade);
|
||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {nonZeroTrade}"); return; }
|
||
|
||
var floatLeg = db.swap_position
|
||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||
.FirstOrDefault();
|
||
Assert.IsNotNull(floatLeg, $"未找到 {nonZeroTrade} 的浮动腿持仓");
|
||
|
||
// 取该交易最近一次 EOD 的 PosiDividendSum 作为期望值(多日稳定 -150,000)
|
||
var lastEod = db.eod_swap_position
|
||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && x.PositionId == floatLeg.PositionId && x.PosiQuantity > 0)
|
||
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||
Assert.IsNotNull(lastEod, $"未找到 {nonZeroTrade} 的有效 EOD");
|
||
Assert.AreNotEqual(0m, lastEod.PosiDividendSum, 0.01m,
|
||
$"前置:{nonZeroTrade} 的 PosiDividendSum 应≠0(本测试专为非0场景设计),实际={lastEod.PosiDividendSum}。" +
|
||
$"若该交易已互换/平仓致归0,请换另一笔 PosiDividendSum≠0 的活交易。");
|
||
|
||
// dealDate 取 lastEod 次日,确保 GetPreEodDividendSum 读到这笔非0 EOD
|
||
var dealDate = lastEod.ValueDate.AddDays(1);
|
||
var service = new RealDbSwapDealService();
|
||
decimal dividend = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, dealDate);
|
||
|
||
// ---- 核心:返回值=真实非0的 PosiDividendSum,不是硬0 ----
|
||
Assert.AreEqual(lastEod.PosiDividendSum, dividend, 0.01m,
|
||
$"[非0场景·方案C验收] {nonZeroTrade} GetPreEodDividendSum 应返回 EOD PosiDividendSum={lastEod.PosiDividendSum}," +
|
||
$"实际={dividend}。该值将同时成为 DividendPending(待结算分红收益,全量口径)。" +
|
||
$"若实际=0,说明 DividendPending 硬0回归未修复,或传导链路被破坏(参见 SwapDealService.GetPreEodDividendSum 注释的口径论证)。");
|
||
|
||
Console.WriteLine($"[非0场景验证通过] {nonZeroTrade}: PosiDividendSum={dividend}(非0)→ DividendIn/DividendPending 均为此值,非硬0。");
|
||
}
|
||
finally { db?.Dispose(); }
|
||
}
|
||
}
|
||
}
|