From 768943636431aeb0cd1bf4d5cf4017cf61c30c2b Mon Sep 17 00:00:00 2001 From: hjhan Date: Tue, 14 Jul 2026 17:12:34 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20=E8=A1=A5=20CalcDailySimpleIntere?= =?UTF-8?q?st=20=E5=85=A8=E8=85=BF=E6=A8=A1=E5=BC=8F+=E6=97=A5=E7=BB=88?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A6=86=E7=9B=96=EF=BC=8C=E9=94=81=E5=AE=9A?= =?UTF-8?q?=E6=97=A0=E6=9B=B4=E5=A4=A7=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求确认 CalcDailySimpleInterest 所有调用方(含日终)覆盖完整性。 代码事实纠正:日终(settment=true)走 CalcEodInterest → CalcDailySimpleInterestByEod (closePercent 硬编码 1m、且该函数从不改写 InterestPrincipal),根本不调用本函数; 本函数唯一真实调用链 = GetInterests(settment=false) → CalcUnwindInterest。 故'含日终'的正确命题是:日终不受此 bug 影响,应有用例锁定该不变量。 新增 CalcByMode 驱动各 InterestMode + 日终路径(rest_days=7, closePercent<1): - 追加预付金(6)/多头存续(7)/空头存续(8)/合约名义本金规模(2):断言线性 closePrincipal - 固定值(1):强制 newClosePercent=1,对平仓比例免疫,断言恒=Fix - 日终 预付金(5)/标的期初全价(9):断言结果恒为线性 closePrincipal(证明 ByEod 正确变体不受影响) SwapUnwindPrepayPrincipalBugTdd 现 19 绿;SwapModule 全量 196 通过/4 跳过,无回归。 --- .../SwapUnwindPrepayPrincipalBugTdd.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs index f0493882..e1124ec7 100644 --- a/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs +++ b/UnitTestProject/Modules/SwapModule/SwapUnwindPrepayPrincipalBugTdd.cs @@ -427,5 +427,141 @@ namespace YLErp.Modules.SwapModule Assert.IsTrue(fe.InterestAmount < fix, "利息基数必须为保证金维度(远小于 fix),证明 orginPv 已用预付金自身 Fix,而非交易名义本金 notional"); } + + // ===== 覆盖完整性补强:所有单利腿模式 + 日终路径 ===== + // 调用链事实(已用代码确认): + // CalcDailySimpleInterest 的唯一真实调用链 = GetInterests(settment=false) → CalcUnwindInterest → 本函数。 + // 日终(settment=true)走 CalcEodInterest → CalcDailySimpleInterestByEod(closePercent 硬编码 1m、 + // 且该函数从不改写 InterestPrincipal),根本不调用本函数。故"含日终"的正确命题是: + // 日终不受本 bug 影响,且应有用例锁定这一不变量。 + // 本组用同一入口驱动各 InterestMode 在 closePercent<1 + rest_days=7 多天场景,断言 + // InterestPrincipal = closePrincipal(线性),捕捉任何指数级回归;并显式加日终(settment=true)用例, + // 断言日终结果恒为线性 closePrincipal(证明日终不受盘中 bug 影响,与正确的 ByEod 变体对齐)。 + private swap_flow_event CalcByMode(int mode, decimal baseP, decimal closePercent, int restDays = 7, bool eodPath = false) + { + 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-MODE-COV", ClientId = 999998, + TradeType = "收益互换", TradeDate = ProdPosiStart, StartDate = ProdPosiStart, + ExerciseDate = ProdUnwindDate.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", + StockEqvNotional = (double)baseP, Notional = (double)baseP, + trade_extend = extend + }; + bool isPrepayOrFixed = mode == (int)InterestModeEnum.初始预付金 + || mode == (int)InterestModeEnum.追加预付金 + || mode == (int)InterestModeEnum.固定值; + var position = new swap_position + { + id = 3003, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = mode, + InterestRateDefault = 0m, + InterestPrincipalFix = isPrepayOrFixed ? baseP : 0m, + 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 = "[]" + }; + // 使 dynomicPrincipal = posiPrincipal:eod.TdInterestPrincipal = orginPv(=baseP), + // 非预付金腿 orginPv 传 baseP;预付金/固定值腿 orginPv 被内部对齐为 Fix=baseP(同样成立)。 + var eodPos = new List + { + new eod_swap_position + { + id = 30, SwapTradeId = 1, PositionId = 3003, + ValueDate = ProdEodValueDate, + TdInterestPrincipal = baseP, + PosiNotionalValue = baseP, + InterestProfitSum = 0m, FloatRate = 0m + } + }; + var interests = _svc.GetInterests(td, td.trade_extend, ProdUnwindDate, ProdUnwindDate, + eodPos, new List { position }, + baseP, baseP, baseP, baseP * closePercent, closePercent, + (int)SwapEventTypeEnum.平仓, + false, false, 0, baseP, false, settment: eodPath, newCalcLast: false, closeList: null); + Assert.AreEqual(1, interests.Count, $"mode={mode} 应生成 1 条 flow_event"); + return interests[0]; + } + + // ---- 追加预付金(6):与初始预付金(5)同源修复,显式覆盖避免遗漏 ---- + [TestMethod] + public void 追加预付金腿_盘中_部分平仓重置周期7天_应线性缩放() + { + var fe = CalcByMode((int)InterestModeEnum.追加预付金, ProdPrepayFix, 0.5m); + Assert.AreEqual(4_590_000m, fe.InterestPrincipal, "追加预付金 50% 应=Fix×0.5(与初始预付金同源修复)"); + var fe1 = CalcByMode((int)InterestModeEnum.追加预付金, ProdPrepayFix, 0.1m); + Assert.AreEqual(918_000m, fe1.InterestPrincipal, "追加预付金 10% 应=Fix×0.1"); + } + + // ---- 多头/空头存续名义本金(7/8):经同一 CalcDailySimpleInterest,需证明修复通用 ---- + [TestMethod] + public void 多头存续名义本金腿_盘中_部分平仓重置周期7天_应线性缩放() + { + const decimal baseP = 2_000_000m; + var fe = CalcByMode((int)InterestModeEnum.多头存续名义本金, baseP, 0.5m); + Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "多头存续 50% 应=posiLong×0.5"); + var fe1 = CalcByMode((int)InterestModeEnum.多头存续名义本金, baseP, 0.1m); + Assert.AreEqual(200_000m, fe1.InterestPrincipal, "多头存续 10% 应=posiLong×0.1"); + } + + [TestMethod] + public void 空头存续名义本金腿_盘中_部分平仓重置周期7天_应线性缩放() + { + const decimal baseP = 2_000_000m; + var fe = CalcByMode((int)InterestModeEnum.空头存续名义本金, baseP, 0.5m); + Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "空头存续 50% 应=posiShort×0.5"); + var fe1 = CalcByMode((int)InterestModeEnum.空头存续名义本金, baseP, 0.1m); + Assert.AreEqual(200_000m, fe1.InterestPrincipal, "空头存续 10% 应=posiShort×0.1"); + } + + // ---- 合约名义本金规模(2):CalcNotionalByMode 默认分支(posiNotional×cp) ---- + [TestMethod] + public void 合约名义本金规模腿_盘中_部分平仓重置周期7天_应线性缩放() + { + const decimal baseP = 2_000_000m; + var fe = CalcByMode((int)InterestModeEnum.合约名义本金规模, baseP, 0.5m); + Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "合约名义本金规模 50% 应=posiNotional×0.5"); + } + + // ---- 固定值(1):CalcNotionalByMode 强制 newClosePercent=1,对 closePercent 免疫(输入 0.5 也不缩放) ---- + [TestMethod] + public void 固定值腿_盘中_部分平仓_对平仓比例免疫_返回Fix本金() + { + const decimal baseP = 2_000_000m; + var fe = CalcByMode((int)InterestModeEnum.固定值, baseP, 0.5m); + Assert.AreEqual(baseP, fe.InterestPrincipal, "固定值腿 newClosePercent=1,InterestPrincipal 恒=Fix,不随平仓比例缩放"); + } + + // ---- 日终路径(settment=true):证明走 CalcDailySimpleInterestByEod,结果恒为线性 closePrincipal,不受盘中 bug 影响 ---- + [TestMethod] + public void 日终_预付金腿_部分平仓_结果应线性且不受盘中bug影响() + { + var fe = CalcByMode((int)InterestModeEnum.初始预付金, ProdPrepayFix, 0.5m, eodPath: true); + Console.WriteLine($"[TDD][EOD 预付金50%] InterestPrincipal={fe.InterestPrincipal} (期望={4_590_000m})"); + Assert.AreEqual(4_590_000m, fe.InterestPrincipal, "日终预付金 50% 应=Fix×0.5(ByEod 正确变体,closePercent 走 closePrincipal 线性)"); + var fe1 = CalcByMode((int)InterestModeEnum.初始预付金, ProdPrepayFix, 0.1m, eodPath: true); + Assert.AreEqual(918_000m, fe1.InterestPrincipal, "日终预付金 10% 应=Fix×0.1"); + } + + [TestMethod] + public void 日终_非预付金腿_部分平仓_结果应线性且不受盘中bug影响() + { + const decimal baseP = 2_000_000m; + var fe = CalcByMode((int)InterestModeEnum.标的期初全价, baseP, 0.5m, eodPath: true); + Console.WriteLine($"[TDD][EOD 标的期初全价50%] InterestPrincipal={fe.InterestPrincipal} (期望={1_000_000m})"); + Assert.AreEqual(1_000_000m, fe.InterestPrincipal, "日终非预付金腿 50% 应=名义本金×0.5(ByEod 正确,不受影响)"); + } } }