From 83a55dab33626cfd5e0d9e2318907c6ca7e42f72 Mon Sep 17 00:00:00 2001 From: hjhan Date: Tue, 14 Jul 2026 16:33:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(swap):=20=E4=BF=AE=E5=A4=8D=E5=B9=B3?= =?UTF-8?q?=E4=BB=93=E9=A2=84=E4=BB=98=E9=87=91=E5=BA=94=E8=BF=94=E8=BF=98?= =?UTF-8?q?=E6=9C=AC=E9=87=91=E8=A2=AB=20closePercent^N=20=E6=8C=87?= =?UTF-8?q?=E6=95=B0=E7=BA=A7=E7=BC=A9=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:CalcDailySimpleInterest 单利非重置日 else 分支曾把已×closePercent 的 InterestPrincipal 回填给计息基数 tdDynomicPrincipal,使下一个非重置日再乘一次, 累积成 InterestPrincipal = Fix × closePercent^N(N=计息天数),部分平仓时指数级缩小。 100% 因 1^N=1 不显现,故此前漏测。 修复:非重置日与重置日、日终 CalcDailySimpleInterestByEod 三者对齐—— 显示本金 InterestPrincipal = 基数×closePercent(只缩放一次),计息基数不缩放。 TDD 铁证(GLMS-20260701-0006, interest_rest_days=7, Fix=9,180,000): - 修复前 50%→71,718.75(=Fix×0.5^7)、10%→0.918(=Fix×0.1^7) - 修复后 50%→4,590,000、10%→918,000、100%→9,180,000 新增 CalcUnwindMultiDay helper + 3 用例,SwapUnwindPrepayPrincipalBugTdd 共 9 绿, SwapModule 全量 186 通过无回归。 --- .../SwapUnwindPrepayPrincipalBugTdd.cs | 105 ++++++++++++++++++ .../Modules/SwapModule/SwapDealService.cs | 8 +- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs index fcaa469f..ccf6c195 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs @@ -212,6 +212,111 @@ namespace YLErp.Modules.SwapModule Assert.AreEqual(100_000m, total, "多次部分平仓合计应=保证金本金 10万"); } + // ---- 盘中路径 CalcDailySimpleInterest 的 closePercent^N 指数级缩小 bug ---- + // 生产铁证 GLMS-20260701-0006:预付金腿 Fix=9,180,000、interest_rest_days=7、单利、不计息。 + // 平仓弹窗(swaptrade2/GetUnwindInterestList → 盘中路径 CalcDailySimpleInterest)返回: + // 100% → 9,180,000 (对) 50% → 71,718.75 (错) 10% → 0.918 (错) + // 数学关系精确成立:9,180,000×0.5^7 = 71,718.75、9,180,000×0.1^7 = 0.918。 + // 根因:CalcDailySimpleInterest 非重置日 else 分支 + // flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent; + // tdDynomicPrincipal = flowEvent.InterestPrincipal; // ★把"已×closePercent"的值回填 + // 使下一个非重置日再乘一次 closePercent → InterestPrincipal = Fix × closePercent^N(N=计息天数), + // 而正确应为 Fix × closePercent(线性,与日终 CalcDailySimpleInterestByEod:1164-1165 只乘一次一致)。 + // 现有 6 个用例 interest_rest_days=1 且 UnwindDate=StartDate+1(calcDays=1),循环首尾都被 continue 跳过、 + // 从不进 else,故漏掉此 bug;本组用例用 restDays=7、跨多日、带 eod 归档触发 else 累积复现之。 + private const decimal ProdPrepayFix = 9_180_000m; + private static readonly DateTime ProdPosiStart = new(2026, 7, 2); + private static readonly DateTime ProdEodValueDate = new(2026, 7, 4); + private static readonly DateTime ProdUnwindDate = new(2026, 7, 13); + + /// + /// 盘中路径复现:restDays=7、PosiStart→Unwind 跨 11 天、eod 归档到 07-04。 + /// 与生产 GLMS-20260701-0006 完全对齐,buggy 代码产出 Fix × closePercent^7。 + /// + private swap_flow_event CalcUnwindMultiDay(decimal closePercent, decimal fix = ProdPrepayFix, int restDays = 7, + decimal rate = 0m) + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "10", // 算头不算尾(与生产一致) + SettlementRules = 0 + }) + }; + var td = new trade + { + id = 1, TradeNumber = "UT-PREPAY-EXP", ClientId = 999998, + TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart, + ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", + StockEqvNotional = (double)fix, Notional = (double)fix, + trade_extend = extend + }; + var position = new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.初始预付金, + InterestRateDefault = rate, InterestPrincipalFix = fix, + PosiStartDate = ProdPosiStart, PosiMatuirityDate = ProdUnwindDate.AddYears(1), + IsInitial = true, Invalid = false, InterestType = (int)InterestTypeEnum.单利, + IsAnnualized = true, interest_rest_days = restDays, + interest_rule = 0, FloatRateUnderlyingCode = null, + InterestSwapInterval = "[]" + }; + var eod = new List + { + new eod_swap_position + { + id = 7, SwapTradeId = 1, PositionId = 1001, + ValueDate = ProdEodValueDate, + TdInterestPrincipal = fix, // 生产 eod_swap_position(35774) TdInterestPrincipal=9,180,000 + PosiNotionalValue = fix, + InterestProfitSum = 0m, FloatRate = 0m + } + }; + var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate, + eod, new List { position }, + fix, fix, fix, fix, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, fix, false, settment: false, newCalcLast: false, closeList: null); + Assert.AreEqual(1, interests.Count, "预付金腿应生成 1 条 flow_event"); + return interests[0]; + } + + [TestMethod] + public void 部分平仓50_盘中重置周期7天_应返还本金应线性缩放而非指数级() + { + var fe = CalcUnwindMultiDay(0.5m); + Console.WriteLine($"[TDD][盘中50%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=71,718.75, 期望=4,590,000)"); + // 正确:Fix × closePercent = 9,180,000 × 0.5 = 4,590,000(100%返 9,180,000 的一半)。 + // buggy:Fix × 0.5^7 = 71,718.75(生产实测),被指数级缩小 ~64 倍。 + Assert.AreEqual(4_590_000m, fe.InterestPrincipal, + "50% 平仓: 应返还本金应=Fix×0.5=4,590,000,不应被 closePercent^7 缩成 71,718.75"); + } + + [TestMethod] + public void 部分平仓10_盘中重置周期7天_应返还本金应线性缩放而非指数级() + { + var fe = CalcUnwindMultiDay(0.1m); + Console.WriteLine($"[TDD][盘中10%] 实测 InterestPrincipal={fe.InterestPrincipal} (buggy=0.918, 期望=918,000)"); + // 正确:Fix × 0.1 = 918,000。buggy:Fix × 0.1^7 = 0.918(生产实测),缩小 100 万倍。 + Assert.AreEqual(918_000m, fe.InterestPrincipal, + "10% 平仓: 应返还本金应=Fix×0.1=918,000,不应被 closePercent^7 缩成 0.918"); + } + + [TestMethod] + public void 全平_盘中重置周期7天_应返还本金应等于保证金本金() + { + // closePercent=1 → 1^N=1,指数 bug 对 100% 无影响(故用户看 100% 正常),此用例锚定不回归。 + var fe = CalcUnwindMultiDay(1m); + Console.WriteLine($"[TDD][盘中100%] 实测 InterestPrincipal={fe.InterestPrincipal} (期望=9,180,000)"); + Assert.AreEqual(ProdPrepayFix, fe.InterestPrincipal, + "100% 平仓: 应返还本金应=Fix=9,180,000(closePercent=1 时指数 bug 不显现,须保持正确)"); + } + [TestMethod] public void 多次部分平仓_计息基数也被根因修复_利息基于保证金本金() { diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 12b6442c..51f0ae9b 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -1034,8 +1034,14 @@ namespace YLErp.Modules.SwapModule } else { + // 单利:计息基数(tdDynomicPrincipal)逐日恒定,非重置日与重置日对本金的处理必须一致—— + // 显示本金 InterestPrincipal = 基数 × closePercent(线性,只缩放一次), + // 计息基数 TdInterestPrincipal = 基数(不缩放)。 + // 修复前此处曾写 tdDynomicPrincipal = flowEvent.InterestPrincipal(已含 closePercent), + // 使下一个非重置日再乘一次 closePercent,累积成 InterestPrincipal = Fix × closePercent^N, + // 导致部分平仓"应返还本金"被指数级缩小(50%→Fix×0.5^7、10%→Fix×0.1^7)。 + // 与日终 CalcDailySimpleInterestByEod(baseInterestPrincipal 只乘一次)对齐。 flowEvent.InterestPrincipal = tdDynomicPrincipal * closePercent; - tdDynomicPrincipal = flowEvent.InterestPrincipal; TdInterestPrincipal = tdDynomicPrincipal; } flowEvent.FloatRate = Convert.ToDecimal(floatRate);