From bfcfc74717b8aeeaedd39e91783fd8433b8813be Mon Sep 17 00:00:00 2001 From: hjhan Date: Tue, 14 Jul 2026 14:46:42 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=B9=B3=E4=BB=93=E9=A2=84=E4=BB=98?= =?UTF-8?q?=E9=87=91bug=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E8=B4=B4?= =?UTF-8?q?=E5=90=88=E7=94=9F=E4=BA=A7=E9=93=81=E8=AF=81=EF=BC=88=E7=B2=BE?= =?UTF-8?q?=E7=A1=AEnotional=20+=20=E4=B8=8D=E8=AE=A1=E6=81=AF=E6=96=AD?= =?UTF-8?q?=E8=A8=80=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 客户截图级用例 notional 由反推的 306,191,860.22 更正为生产精确值 306,191,860.26,使 2*Fix-Notional = -287,820,348.64 与生产 event15997 精确 0 误差。 - 生产该预付金腿三条 swap_flow_event 的 InterestAmount 全=0(债券类预付金腿不计息), 仅 InterestPrincipal 中招;故客户级/Trade1813 用例改断言 InterestAmount==0、用 rate=0 贴合生产。 - 显式带息加固用例(rate=0.01,验证计息基数也基于保证金本金自身)保留,但注释标明为 合成场景、非该笔生产的真实症状,避免与生产现象混淆。 - 固化生产双平仓记录佐证:同腿"盘中错(event15997)/EOD对(event15998)"两条平仓, 恰好印证修复方向(盘中 orginPv 对齐 EOD=Fix)正确。 --- .../SwapUnwindPrepayPrincipalBugTdd.cs | 59 +++++++++++-------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs index 32912c84..fcaa469f 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs @@ -70,14 +70,14 @@ namespace YLErp.Modules.SwapModule }; } - private static swap_position MakePrepayPosition(decimal fix = PrepayPrincipal) + private static swap_position MakePrepayPosition(decimal fix = PrepayPrincipal, decimal rate = 0.01m) { return new swap_position { id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.初始预付金, - InterestRateDefault = 0.01m, InterestPrincipalFix = fix, + InterestRateDefault = rate, InterestPrincipalFix = fix, PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1, @@ -104,11 +104,11 @@ namespace YLErp.Modules.SwapModule /// 客户/真实库场景:自定义 标的名义本金(notional) 与 保证金本金(fix)。 /// orginPv 用 notional(与 GetUnwindInterests 行为一致:lastEod.NotionalValue ?? stockEqvNotional)。 /// - private swap_flow_event CalcUnwindWith(decimal closePercent, List eodPositions, decimal notional, decimal fix) + private swap_flow_event CalcUnwindWith(decimal closePercent, List eodPositions, decimal notional, decimal fix, decimal rate = 0.01m) { eodPositions ??= new List(); var td = MakeTrade(notional); - var position = MakePrepayPosition(fix); + var position = MakePrepayPosition(fix, rate); var interests = _svc.GetInterests(td, td.trade_extend, UnwindDate, UnwindDate, eodPositions, new List { position }, notional, notional, notional, notional, closePercent, @@ -152,31 +152,41 @@ namespace YLErp.Modules.SwapModule [TestMethod] public void 客户截图级_全平_应返还本金应等于保证金本金() { - // 客户截图症状:支付预付金 9,185,755.81;平仓"应返还本金"=-287,820,348.6 - // 反推标的名义本金 = 2*9,185,755.81 + 287,820,348.6 = 306,191,860.22(保证金比例 3%,正常) - const decimal notional = 306_191_860.22m; + // 生产铁证(用户提供真实交易):TradeAmount=3亿,StockEqvNotional=306,191,860.26, + // StructureType=普通债券类收益互换;预付金腿 swap_position id=34009 InterestMode=5 + // InterestPrincipalFix=9,185,755.81。 + // swap_flow_event(该腿, mode5) 三条: + // 9202 EventId=null dir2 IP=9,185,755.81 (建仓支付预付金 ✓) + // 9489 EventId=15997 dir1 IP=-287,820,348.64 (平仓, 盘中路径 BUG ✗) + // 9492 EventId=15998 dir1 IP=9,185,755.81 (平仓, EOD正确路径 ✓) + // 同一腿出现"盘中错 / EOD对"两条平仓记录,恰好佐证修复方向(盘中 orginPv 对齐 EOD=Fix)正确。 + // 根因复现:2*Fix - Notional = 2*9,185,755.81 - 306,191,860.26 = -287,820,348.64(与生产 15997 精确 0 误差)。 + // 该预付金腿三条 event 的 InterestAmount 全=0(债券类预付金腿不计息), + // 故本笔生产仅 InterestPrincipal 中招、计息基数未受影响 → rate=0 贴合生产。 + const decimal notional = 306_191_860.26m; const decimal fix = 9_185_755.81m; - var fe = CalcUnwindWith(1m, null, notional, fix); + var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0m); Console.WriteLine($"[TDD][客户] 实测 InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount} (期望Principal={fix})"); Assert.AreEqual(fix, fe.InterestPrincipal, - "客户级: 应返还本金应=保证金本金 9,185,755.81,不应被算成 -287,820,348.6"); - Assert.IsTrue(fe.InterestAmount > 0 && fe.InterestAmount < fix, - "客户级: 利息基数应基于保证金本金(小额正),证明 orginPv 已对齐 Fix 而非交易名义本金"); + "客户级: 应返还本金应=保证金本金 9,185,755.81,不应被算成 -287,820,348.64"); + Assert.AreEqual(0m, fe.InterestAmount, + "客户级: 该预付金腿不计息,InterestAmount 应=0(与生产三条 event 全为 0 一致);仅 InterestPrincipal 中招"); } [TestMethod] public void 真实库Trade1813_全平_应返还本金应等于保证金本金() { // 测试库 Trade=1813 / Pos=34204:Fix=35,140,Notional=12,100,000, - // 实际存储 InterestPrincipal=-12,029,720.00(=2*35,140-12,100,000,公式精确 0 误差) + // 实际存储 InterestPrincipal=-12,029,720.00(=2*35,140-12,100,000,公式精确 0 误差)。 + // 同属债券类预付金腿(与生产同模式,不计息),rate=0 贴合生产,仅验证 InterestPrincipal 修复。 const decimal notional = 12_100_000m; const decimal fix = 35_140m; - var fe = CalcUnwindWith(1m, null, notional, fix); + var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0m); Console.WriteLine($"[TDD][Trade1813] 实测 InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount} (期望Principal={fix})"); Assert.AreEqual(fix, fe.InterestPrincipal, "Trade1813: 应返还本金应=保证金本金 35,140,不应被算成 -12,029,720.00"); - Assert.IsTrue(fe.InterestAmount > 0 && fe.InterestAmount < fix, - "Trade1813: 利息基数应基于保证金本金(小额正),证明 orginPv 已对齐 Fix 而非交易名义本金"); + Assert.AreEqual(0m, fe.InterestAmount, + "Trade1813: 同属债券类预付金腿不计息,InterestAmount 应=0;仅 InterestPrincipal 中招"); } // ---- 多次部分平仓(验证最小修复是否覆盖"多次部分成交")---- @@ -188,11 +198,11 @@ namespace YLErp.Modules.SwapModule // (真实系统中每次部分平仓后 position.InterestPrincipalFix 会被扣减,下一笔用剩余值)。 // 根因修复后:InterestPrincipal 由利息公式基于 Fix 正确得出 = fix * closePercent。 decimal total = 0; - var r1 = CalcUnwindWith(0.3m, null, 306_191_860.22m, 100_000m); + var r1 = CalcUnwindWith(0.3m, null, 306_191_860.26m, 100_000m); total += r1.InterestPrincipal; - var r2 = CalcUnwindWith(0.5m, null, 306_191_860.22m, 70_000m); // 剩余 7万 + var r2 = CalcUnwindWith(0.5m, null, 306_191_860.26m, 70_000m); // 剩余 7万 total += r2.InterestPrincipal; - var r3 = CalcUnwindWith(1.0m, null, 306_191_860.22m, 35_000m); // 剩余 3.5万 + var r3 = CalcUnwindWith(1.0m, null, 306_191_860.26m, 35_000m); // 剩余 3.5万 total += r3.InterestPrincipal; Console.WriteLine($"[TDD][多次部分] r1={r1.InterestPrincipal} r2={r2.InterestPrincipal} r3={r3.InterestPrincipal} 合计={total}"); @@ -205,17 +215,18 @@ namespace YLErp.Modules.SwapModule [TestMethod] public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金() { - // 根因修复后:预付金腿的 orginPv 已对齐为其自身保证金(Fix), - // 不仅"应返还本金"(InterestPrincipal) 正确,逐日利息计息基数也正确 - // (基于保证金本金,而非交易名义本金),故 InterestAmount 应为小额正。 - const decimal notional = 306_191_860.22m; + // 显式带息加固用例(合成,非用户那笔生产的真实症状): + // 用户那笔生产(3亿债券类TRS)预付金腿不计息(InterestAmount 全=0),仅 InterestPrincipal 中招; + // 本例用 rate=0.01 构造"若该腿计息"的场景,验证根因修复后计息基数也基于保证金本金自身 + // (而非交易名义本金):InterestAmount 为小额正、且 < fix。 + const decimal notional = 306_191_860.26m; const decimal fix = 9_185_755.81m; - var fe = CalcUnwindWith(1m, null, notional, fix); + var fe = CalcUnwindWith(1m, null, notional, fix, rate: 0.01m); Assert.AreEqual(fix, fe.InterestPrincipal, "显示值(应返还本金)已=保证金本金"); Console.WriteLine($"[TDD][计息基数] InterestPrincipal={fe.InterestPrincipal} InterestAmount={fe.InterestAmount}"); Assert.IsTrue(fe.InterestAmount > 0, - "根因修复后: 预付金腿 InterestAmount 应基于保证金本金算出小额正值(约 fix*rate),不再是巨负"); + "根因修复后(显式带息): 预付金腿 InterestAmount 应基于保证金本金算出小额正值(约 fix*rate),不再是巨负"); Assert.IsTrue(fe.InterestAmount < fix, "利息基数必须为保证金维度(远小于 fix),证明 orginPv 已用预付金自身 Fix,而非交易名义本金 notional"); }