Files
zszq-trs/UnitTestProject/Modules/SwapModule/GLMS20260105PartialCloseDividendBugTest.cs
T
张名锐 2ee38384ac fix(swap): 修复预付金腿计息基数计算问题
- 在多个单元测试文件中添加InterestIncomeSum字段以保持数据一致性
- 修改GLMS20260105PartialCloseDividendBugTest测试,改进EOD快照验证逻辑
- 修复SwapDealService中预付金腿的orginPv计算逻辑,使用上一日保证金本金作为基准
- 更新SwapPositionComposeScenarioTest中的测试数据结构和利率设置
- 修正平仓日利息计算精度问题,使用Math.Round确保计算准确性
2026-08-09 22:28:40 +08:00

223 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 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,0002/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 恰好=03/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(); }
}
}
}