Files
zszq-trs/UnitTestProject/Modules/SwapModule/ApplySwapTradeClosePercentBugTest.cs
T
hjhan e261137c1d fix(swap): 修复多次部分平仓 ClosePercent 口径(A占期初→B占剩余)转换缺失
- SwapDealService.ApplySwapTrade 入口补 ToRemainingClosePercent(A→B) 转换,与 SwapUnwind 保持一致;缺此转换导致 SaveSwapDealInternal 的 B→A 还原出错(GLMS-20260701-0006:第二次部分平仓50%被错误还原为32.5%)
- unwindSwapTrade.js 改用 swapCalc.calcCloseQtyByOriginalPercent 的 roundHalfAwayFromZero,规避 JS 浮点精度偏差(32500000*(0.5/0.65) 应为 25000000)
- SwapUnwind.cshtml 引入 swapCalc.js
- 新增 ApplySwapTradeClosePercentBugTest 回归测试;补充 swapCalc.test.js 精度用例与 GLMS20260701DbDiagnoseTest 诊断测试([Ignore])
2026-07-17 10:59:18 +08:00

181 lines
9.0 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>
/// GLMS-20260701-0006 多次部分平仓后提前终止 Tab 序号2 平仓比例显示 32.50% 而非 50% 的回归测试
/// ================================================================================
/// 根因:SwapDealService.ApplySwapTrade 缺少 A→B 口径转换。
/// - SwapUnwind 在入口处将 ClosePercent 从口径A(占期初) 转为 口径B(占剩余)SaveSwapDealInternal 落库时 B→A 还原。
/// - ApplySwapTrade 没有做 A→B 转换,导致 SaveSwapDealInternal 的 B→A 还原出错:
/// 0.50(A) → ToOriginalClosePercent(0.50, 50000000, 32500000) = 0.50*32500000/50000000 = 0.325 ❌
/// - 修复后:0.50(A) → ToRemainingClosePercent → 0.769(B) → ToOriginalClosePercent → 0.50(A) ✅
///
/// 测试策略:
/// 1) 纯函数测试:验证 A→B→A 往返转换的正确性
/// 2) ApplySwapTrade 集成测试:验证 SaveSwapDeal 收到的 ClosePercent 已转为口径B
/// </summary>
[TestClass]
public class ApplySwapTradeClosePercentBugTest
{
// GLMS-20260701-0006 真实数据
private const decimal OriginalNotional = 50_000_000m; // 期初名义本金
private const decimal RemainingAfter1st = 32_500_000m; // 首次平35%后剩余
private const decimal FirstClosePercent = 0.35m; // 第一次平仓比例(口径A)
private const decimal SecondClosePercent = 0.50m; // 第二次平仓比例(口径A, 用户输入50%)
// ================================================================
// 1) 纯函数:A→B→A 往返转换应还原原值
// ================================================================
[TestMethod]
public void AC_001_口径转换_往返A到B到A应还原原值()
{
// 第二次部分平仓: 用户输入 50%(口径A)
decimal closePercentA = SecondClosePercent;
// A → BApplySwapTrade/SwapUnwind 入口转换)
decimal closePercentB = SwapDealService.ToRemainingClosePercent(
closePercentA, OriginalNotional, RemainingAfter1st);
// B → ASaveSwapDealInternal 落库还原)
decimal closePercentA_restored = SwapDealService.ToOriginalClosePercent(
closePercentB, OriginalNotional, RemainingAfter1st);
SwapDealTestFactory.AssertDecimalEqual(closePercentA, closePercentA_restored, 1e-10m,
"A→B→A 往返转换应还原原值");
Console.WriteLine($"A={closePercentA}, B={closePercentB}, A_restored={closePercentA_restored}");
}
[TestMethod]
public void AC_002_口径转换_未修复时B到A会得到错误的0_325()
{
// 模拟 bugApplySwapTrade 未做 A→B 转换,直接把 A 传给 SaveSwapDealInternal 的 B→A 还原
decimal closePercentA = SecondClosePercent; // 0.50
// bug 路径:SaveSwapDealInternal 误把 A 当 B 做还原
decimal buggyResult = SwapDealService.ToOriginalClosePercent(
closePercentA, OriginalNotional, RemainingAfter1st);
// 0.50 * 32500000 / 50000000 = 0.325
SwapDealTestFactory.AssertDecimalEqual(0.325m, buggyResult, 1e-10m,
"bug 路径:0.50(A) 被误当 B 做还原 → 0.325");
Assert.AreNotEqual(SecondClosePercent, buggyResult,
"bug 结果 0.325 不等于用户输入 0.50");
Console.WriteLine($"Bug: 0.50(A) 误当 B → ToOriginalClosePercent → {buggyResult} (应为 0.50)");
}
// ================================================================
// 2) ApplySwapTrade 集成测试:验证 SaveSwapDeal 收到的是口径B
// ================================================================
[TestMethod]
public void AC_003_ApplySwapTrade_第二次部分平仓50perc_应将ClosePercent转为口径B()
{
// 模拟 GLMS-20260701-0006 第二次部分平仓的场景
var td = new trade
{
id = 1991,
TradeNumber = "GLMS-20260701-0006",
TradeType = "收益互换",
TradeStatus = "确认成交",
ValidState = "Valid",
StockEqvNotional = (double)RemainingAfter1st, // 32500000
OriginalStockEqvNotional = (double)OriginalNotional, // 50000000
Notional = (double)RemainingAfter1st,
TradeAmount = (double)RemainingAfter1st
};
var service = new TestableSwapDealService(td);
// 前端传入的 UnwindDataClosePercent = 0.50, 口径A
var unwindData = new UnwindData
{
SwapTradeId = td.id,
SwapRealizedPnL = 1000m,
SwapCloseAmount = 1000m,
CloseMethod = (int)CloseMethodEnum.部分平仓,
ClosePercent = SecondClosePercent, // 0.50 (口径A, 用户输入50%)
CloseQty = 25000000m,
CloseNotionalValue = 25000000m, // 50% of original
PositionQty = RemainingAfter1st, // 32500000
NotionalValue = OriginalNotional, // 50000000 (期初)
PosiNotionalValue = RemainingAfter1st, // 32500000 (剩余)
ValueDate = new DateTime(2026, 7, 14),
UnwindDate = new DateTime(2026, 7, 15),
StartDate = new DateTime(2026, 7, 1)
};
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓);
// 验证 SaveSwapDeal 被调用
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "ApplySwapTrade 应调用 SaveSwapDeal");
// 验证传给 SaveSwapDeal 的 ClosePercent 已转为口径B
var savedData = service.SaveSwapDealCalls[0].data;
decimal expectedB = SwapDealService.ToRemainingClosePercent(
SecondClosePercent, OriginalNotional, RemainingAfter1st);
SwapDealTestFactory.AssertDecimalEqual(expectedB, savedData.ClosePercent, 1e-10m,
"ApplySwapTrade 应将 ClosePercent 从口径A转为口径B");
// 关键验证:B 值不应等于 A 值(0.50),也不应等于 bug 值(0.325)
Assert.AreNotEqual(SecondClosePercent, savedData.ClosePercent,
"口径B 不应等于口径A (0.50)");
Assert.AreNotEqual(0.325m, savedData.ClosePercent,
"口径B 不应等于 bug 值 (0.325)");
Console.WriteLine($"输入: ClosePercent(A)={SecondClosePercent}");
Console.WriteLine($"输出: ClosePercent(B)={savedData.ClosePercent}");
Console.WriteLine($"期望: ClosePercent(B)={expectedB}");
Console.WriteLine($"往返还原: ClosePercent(A)={SwapDealService.ToOriginalClosePercent(savedData.ClosePercent, OriginalNotional, RemainingAfter1st)}");
}
[TestMethod]
public void AC_004_ApplySwapTrade_第一次平仓35perc_口径转换正确()
{
// 第一次平仓:remaining = original, 所以 A = B = 0.35
var td = new trade
{
id = 1991,
TradeNumber = "GLMS-20260701-0006",
TradeType = "收益互换",
TradeStatus = "确认成交",
ValidState = "Valid",
StockEqvNotional = (double)OriginalNotional,
OriginalStockEqvNotional = (double)OriginalNotional,
Notional = (double)OriginalNotional,
TradeAmount = (double)OriginalNotional
};
var service = new TestableSwapDealService(td);
var unwindData = new UnwindData
{
SwapTradeId = td.id,
SwapRealizedPnL = 1000m,
SwapCloseAmount = 1000m,
CloseMethod = (int)CloseMethodEnum.部分平仓,
ClosePercent = FirstClosePercent, // 0.35 (口径A)
CloseQty = 17500000m,
CloseNotionalValue = 17500000m,
PositionQty = OriginalNotional,
NotionalValue = OriginalNotional,
PosiNotionalValue = OriginalNotional, // 首次平仓 remaining == original
ValueDate = new DateTime(2026, 7, 6),
UnwindDate = new DateTime(2026, 7, 7),
StartDate = new DateTime(2026, 7, 1)
};
service.ApplySwapTrade(unwindData, (int)SwapEventTypeEnum.平仓);
var savedData = service.SaveSwapDealCalls[0].data;
// 首次平仓 remaining == original → A == B == 0.35
SwapDealTestFactory.AssertDecimalEqual(FirstClosePercent, savedData.ClosePercent, 1e-10m,
"首次平仓 remaining==original → 口径A==口径B==0.35");
Console.WriteLine($"首次平仓: ClosePercent(A=B)={savedData.ClosePercent}");
}
}
}