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 正确,不受影响)"); + } } }