From bb8130698fc8841580bf585f85066df403401c3d Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 20 Aug 2026 13:23:12 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20EQD-6977=20=E7=BD=9A=E6=81=AF?= =?UTF-8?q?=E6=8E=A5=E7=BC=9D=20headless=20=E5=8D=95=E6=B5=8B=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=9B=BA=E5=AE=9A/=E6=B5=AE=E5=8A=A8=E8=85=BF?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20IsPenaltyInterest=20=E6=A0=87=E8=AE=B0=20+?= =?UTF-8?q?=20=E6=89=BF=E6=8E=A5=E6=81=92=E7=AD=89=E5=BC=8F(=E5=85=A8?= =?UTF-8?q?=E6=9C=9F=3D=E5=B7=B2=E7=BB=93+=E7=BD=9A=E6=81=AF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Penalty/PenaltyInterestAppenderTest.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/Penalty/PenaltyInterestAppenderTest.cs diff --git a/UnitTestProject/Modules/SwapModule/Penalty/PenaltyInterestAppenderTest.cs b/UnitTestProject/Modules/SwapModule/Penalty/PenaltyInterestAppenderTest.cs new file mode 100644 index 00000000..9cff946b --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/Penalty/PenaltyInterestAppenderTest.cs @@ -0,0 +1,112 @@ +using YLErp.Modules.SwapModule.Accrual; +using YLErp.Modules.SwapModule.Penalty; + +namespace UnitTestProject.Modules.SwapModule.Penalty +{ + /// + /// EQD-6977 罚息接缝 headless 测试(无 DB:spread/preEod/取价 全部以委托注入)。 + /// 锁定:Append 在融资腿上追加 IsPenaltyInterest=1 的同构罚息流;承接恒等式(全期=已结+罚息)。 + /// + [TestClass] + public class PenaltyInterestAppenderTest + { + private const decimal Notional = 100_000_000m; + private const decimal Rate = 0.0225m; // 冻结 all-in 年化 + private const int AnnualDays = 365; + private static readonly DateTime StartDate = new(2026, 7, 31); + private static readonly DateTime MaturityDate = new(2026, 8, 31); + private static readonly DateTime UnwindDate = new(2026, 8, 25); + + private static trade CreateTrade() + => new() + { + id = 1, TradeNumber = "UT-APPEND", ClientId = 999998, + TradeType = "收益互换", StartDate = StartDate, TradeDate = StartDate, + ExerciseDate = MaturityDate, TradeStatus = "确认成交", ValidState = "Valid" + }; + + private static swap_position FixedLeg(InterestTypeEnum interestType) + => new() + { + id = 1001, SwapTradeId = 1, PosiDirection = 0, InterestDirection = 1, + InterestMode = (int)InterestModeEnum.固定值, InterestRateDefault = Rate, + InterestPrincipalFix = Notional, PosiStartDate = StartDate, PosiMatuirityDate = MaturityDate, + IsInitial = true, Invalid = false, InterestType = (int)interestType, + IsAnnualized = true, interest_rest_days = 7, interest_rule = 0, + FloatRateUnderlyingCode = null, InterestSwapInterval = "[]" + }; + + private static void RunAppend(swap_position p, out List interests, + Func? getSpread = null) + { + getSpread ??= _ => Rate; + interests = new List(); + PenaltyInterestAppender.Append( + CreateTrade(), new List { p }, interests, UnwindDate, AnnualDays, + unwindDaySettled: true, maturityCalcLast: true, + posiNotionalValue: Notional, closePosiNotionalValue: Notional, closePercent: 1m, + getSpread: getSpread, + getPreEodFloatRate: _ => null, + tryGetFixing: (d, code) => (decimal?)Rate); + } + + [TestMethod] + public void 固定腿_single利_追加罚息流且标记列() + { + RunAppend(FixedLeg(InterestTypeEnum.单利), out var interests); + + Assert.AreEqual(1, interests.Count, "应恰好追加 1 笔罚息"); + var e = interests[0]; + Assert.AreEqual(1, e.IsPenaltyInterest, "IsPenaltyInterest 应置 1"); + Assert.AreEqual(SwapPenaltyInterestCalculator.PenaltyEventReason, e.EventReason, "事件原因=罚息"); + // 窗口 (8/25, 8/31] = 6 天(算尾平仓日 + 到期算尾) + var expected = Rate * Notional * 6m / AnnualDays; + Assert.AreEqual((double)expected, (double)e.InterestAmount, 0.0001, "单利罚息=利率×本金×天数/基准"); + } + + [TestMethod] + public void 浮动腿_经取价委托解析冻结率并追加() + { + var p = FixedLeg(InterestTypeEnum.单利); + p.FloatRateUnderlyingCode = "FR007"; // 浮动腿:走 tryGetFixing + p.InterestMode = (int)InterestModeEnum.标的期初全价; + // 浮动腿 all-in = 加点利差(spread) + 指数定盘(fixing);零利差时与固定腿同值 + RunAppend(p, out var interests, getSpread: _ => 0m); + + Assert.AreEqual(1, interests.Count); + Assert.AreEqual(1, interests[0].IsPenaltyInterest); + // 取价委托恒返回 Rate → all-in = 0 + Rate,与固定腿同值 + var expected = Rate * Notional * 6m / AnnualDays; + Assert.AreEqual((double)expected, (double)interests[0].InterestAmount, 0.0001, "浮动腿冻结率=取价委托值(零利差)"); + } + + [TestMethod] + public void 复利_承接恒等式_全期等于已结加罚息() + { + var p = FixedLeg(InterestTypeEnum.复利); + RunAppend(p, out var interests); + + Assert.AreEqual(1, interests.Count); + Assert.AreEqual(1, interests[0].IsPenaltyInterest); + + // 与金标准测试同款 CompoundAccruedTo(AccrualBoundary.Both):全期=已结+罚息 恒等式 + var full = CompoundAccruedTo(MaturityDate, AccrualBoundary.Both); + var elapsed = CompoundAccruedTo(UnwindDate, AccrualBoundary.Both); + Assert.AreEqual((double)full, (double)(elapsed + interests[0].InterestAmount), 0.0001, + "全期(冻结率重放) 应等于 已结 + 罚息;承接量推导正确"); + } + + /// 常率复利重放 [StartDate, endDate],重置段 = 每 7 天(与金标准测试一致)。 + private static decimal CompoundAccruedTo(DateTime endDate, AccrualBoundary boundary) + { + var segs = new List<(DateTime, decimal)>(); + for (var d = StartDate; d <= endDate; d = d.AddDays(7)) segs.Add((d, Rate)); + return CompoundInterestAccrual.AccruePeriod( + notional: Notional, segmentRates: segs, + startDate: StartDate, endDate: endDate, + boundary: boundary, annualDays: AnnualDays, isAnnualized: true, + resetCarryInterest: 0m, realizedInterest: 0m, unwindFraction: 1m, + finalBasis: out _).Accrued; + } + } +}