From 8b66875098d21ef5728305d904c52b766c2827f2 Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 21 Aug 2026 07:09:57 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20EQD-6977=20=E7=BD=9A=E6=81=AF?= =?UTF-8?q?=E5=85=9C=E5=BA=95=E8=A1=A5share<1=E6=81=92=E7=AD=89=E5=BC=8F?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E2=80=94=E2=80=9450%=E5=B9=B3=E4=BB=93+?= =?UTF-8?q?=E6=97=A0preEod=E5=9C=BA=E6=99=AF=E9=92=89merger=E5=A4=8D?= =?UTF-8?q?=E5=88=BBGetInterests=E6=9C=AC=E9=87=91=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E7=9A=84=E6=8E=A5=E7=BC=9D(=E6=A0=87=E7=9A=84=E6=9C=9F?= =?UTF-8?q?=E5=88=9D=E5=85=A8=E4=BB=B7CalcNotional=3DposiNotional=C3=97clo?= =?UTF-8?q?sePercent=20vs=20=E9=87=8D=E6=94=BE=E5=9F=BA=E6=95=B0=E6=8C=89c?= =?UTF-8?q?losePosiNotionalValue=E7=BC=A9=E6=94=BE)=EF=BC=9B=E7=8E=B0?= =?UTF-8?q?=E6=9C=89=E7=94=A8=E4=BE=8B=E5=85=A8closePercent=3D1m=EF=BC=8C?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=E5=8F=A3=E5=BE=84=E8=8B=A5=E6=9C=89=E5=81=8F?= =?UTF-8?q?=E5=B7=AE=E5=9C=A8share=3D1=E4=B8=8B=E4=B8=8D=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E3=80=81share<1=E6=97=B6=E2=91=A0=E6=B7=B7=E5=85=A5=E6=9C=AC?= =?UTF-8?q?=E9=87=91=E5=B7=AE=EF=BC=9B=E5=8F=98=E5=BC=82=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=EF=BC=9ACalcNotional=E4=B8=B4=E6=97=B6=E5=BF=BD=E7=95=A5closeP?= =?UTF-8?q?ercent=E5=94=AF=E6=AD=A4=E7=94=A8=E4=BE=8B=E7=BA=A2(1=E8=B4=A57?= =?UTF-8?q?=E8=BF=87)=E8=AF=81=E6=98=8E=E9=89=B4=E5=88=AB=E5=8A=9B?= =?UTF-8?q?=EF=BC=9BRunFee=E5=8A=A0closePercent=E5=8F=82=E6=95=B0=E9=BB=98?= =?UTF-8?q?=E8=AE=A41m=E5=AD=98=E9=87=8F=E8=B0=83=E7=94=A8=E9=9B=B6?= =?UTF-8?q?=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Penalty/PenaltyBoundaryMatrixTest.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) 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_恒等式成立() {