diff --git a/UnitTestProject/Modules/SwapModule/Penalty/PenaltyBoundaryMatrixTest.cs b/UnitTestProject/Modules/SwapModule/Penalty/PenaltyBoundaryMatrixTest.cs index 6ebb8bf0..bf3dd846 100644 --- a/UnitTestProject/Modules/SwapModule/Penalty/PenaltyBoundaryMatrixTest.cs +++ b/UnitTestProject/Modules/SwapModule/Penalty/PenaltyBoundaryMatrixTest.cs @@ -11,7 +11,9 @@ namespace UnitTestProject.Modules.SwapModule.Penalty /// ② 重置日前一日平仓(② 几乎整段、窗口首段 0 天); /// ③ 到期日恰为重置日(末段 [到期,到期] 1 天); /// ④ 锚点偏离(td.StartDate=7/31 但腿 PosiStartDate=8/3 的延期/存续腿——重置网格整体不同); - /// ⑤ 起息日当天平仓(无 preEod)。 + /// ⑤ 起息日当天平仓(无 preEod); + /// ⑥ 部分平仓 share<1 + 无 preEod 兜底——钉 merger 复刻 GetInterests 本金口径的接缝 + /// (现有用例全部 closePercent=1m,重放基数与复刻本金的口径偏差在 share=1 下不可见)。 /// /// 一致性前提(与现实世界对齐):冻结利率 = 当前重置区间(含 unwind-1 的区间)的在役利率, /// 即"历史末段利率 = 冻结利率";历史各段定盘不同(体现真实 FR007 利率历史)。 @@ -67,7 +69,7 @@ namespace UnitTestProject.Modules.SwapModule.Penalty private static decimal RunFee(trade td, swap_position p, decimal settledAmount, eod_swap_position? preEod, DateTime unwind, bool settled, decimal spread, - decimal interestPrincipal = 0m, bool maturityCalcLast = true) + decimal interestPrincipal = 0m, bool maturityCalcLast = true, decimal closePercent = 1m) { var e = new swap_flow_event { @@ -78,7 +80,8 @@ namespace UnitTestProject.Modules.SwapModule.Penalty PenaltyInterestFeeMerger.Merge( td, new List { p }, new List { e }, unwind, AnnualDays, settled, maturityCalcLast: maturityCalcLast, - posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m, + posiNotionalValue: Notional, closePosiNotionalValue: Notional * closePercent, + closePercent: closePercent, getSpread: _ => spread, getPreEod: _ => preEod, tryGetFixing: (d, c) => spread); return e.InterestFee; } @@ -192,6 +195,30 @@ namespace UnitTestProject.Modules.SwapModule.Penalty "无preEod+已有重置:兜底取事件基数后 ① 精确,全期=实结+罚息(修复前差≈3.17元)"); } + [TestMethod] + public void 部分平仓无preEod兜底_share对齐本金口径_恒等式成立() + { + // 接缝守卫:merger 的 closePrincipal 走 CalcNotional 复刻 GetInterests 口径 + // (标的期初全价 = posiNotional×closePercent),而重放基数由调用方以 + // closePosiNotionalValue 缩放——两处口径若有偏差,share=1 时不可见、 + // share<1 时 ① 里会混入本金差。本用例以 50% 平仓钉死该对齐。 + var start = new DateTime(2026, 8, 5); var unwind = new DateTime(2026, 8, 20); var maturity = new DateTime(2026, 9, 30); + var hist = new decimal[] { 0.0216m, 0.0144m }; // 8/5 段 2.16% / 8/19 段 1.44%(=冻结),14 天重置 + var share = 0.5m; + var closedNotional = Notional * share; + // 被平份额的实结与重放基数:复利对 notional 线性,直接按半额本金重放 + var elapsed = AccrueOnGrid(start, unwind, AccrualBoundary.StartOnly, hist, notional: closedNotional, period: 14); + var replayFinalBasis = closedNotional + AccrueOnGrid(start, new DateTime(2026, 8, 18), AccrualBoundary.Both, hist, notional: closedNotional, period: 14); + + var fee = RunFee(CreateTrade(start, maturity), CompoundLeg(start, maturity, hist[^1], periodDays: 14), elapsed, + preEod: null, unwind: unwind, settled: false, spread: hist[^1], + interestPrincipal: replayFinalBasis, maturityCalcLast: false, closePercent: share); + + var full = AccrueOnGrid(start, maturity, AccrualBoundary.StartOnly, hist, notional: closedNotional, period: 14); + Assert.AreEqual((double)full, (double)(elapsed + fee), 0.01, + "部分平仓+无preEod:兜底①按被平份额缩放精确,全期(被平份额)=实结+罚息(口径漂移时此式必挂)"); + } + [TestMethod] public void 起息日当天平仓_无preEod_恒等式成立() {