From f069c38d488dde491ad81171daa3efe599398d2c Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 19 Aug 2026 09:26:30 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20EQD-6968=20=E8=A1=A5=E7=AE=97?= =?UTF-8?q?=E5=B0=BE=E4=BE=9D=E8=B5=96=E5=AE=88=E5=8D=AB=E2=80=94=E2=80=94?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E6=97=A5=E7=BC=BA=E4=BB=B7=E4=BB=8D=E6=8B=A6?= =?UTF-8?q?+newCalcLast=E8=A6=86=E7=9B=96=E7=AE=97=E5=B0=BE=E4=BB=8D?= =?UTF-8?q?=E6=8B=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 防'不算尾放宽'误扩到依赖场景(方案一场景4): - Guard_TailCalced×2:算尾(11)+当日重置日+当日缺价 → 必须仍抛(复利/单利) - Baseline_TailCalced:算尾+当日有价 → 成功(对照) - Guard_NewCalcLast:交易10不算尾但本次平仓指定算尾 → effectiveCalcLast=true 仍拦 - Baseline_NewCalcLast:指定算尾+有价 → 成功 Run 运行器扩参:calcMode/interestRule/newCalcLast/market(本提交仅用 calcMode/newCalcLast) 验证:GLMS20260817Fr007UnwindMorningTest 17/17 内存全绿 --- .../GLMS20260817Fr007UnwindMorningTest.cs | 83 ++++++++++++++++--- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs index b05558e0..4c678b3e 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs @@ -14,6 +14,11 @@ namespace YLErp.Modules.SwapModule /// /// 核心语义:算头不算尾(calcLast=false)时 endDate 当天不计息,其 FR007 利率不参与计息。 /// 缺价时跳过取价(currentFloat 保持不变),不回退取其他日期利率,不告警。 + /// + /// 守卫矩阵(防"放宽过头",对应 EQD-6968 方案一四场景): + /// Guard_*:算尾("11"或newCalcLast=true)+当日重置日+当日缺价 → 必须仍拦截(正确依赖); + /// PrevBizDay_*:interest_rule=-1(前一营业日基准)→ 取价日回拨,当日未发布也放行(场景2); + /// TailCalced_NonResetDay_*:算尾+当日非重置日 → 当日价未消费,缺价放行(场景3)。 /// [TestClass] public class GLMS20260817Fr007UnwindMorningTest @@ -38,20 +43,23 @@ namespace YLErp.Modules.SwapModule { private readonly HashSet _omit; private readonly double _closeRate; + private readonly Dictionary _market; public readonly List PricedDates = new(); - public StubSwapDealService(OptUserInfo optUser, IEnumerable omit, double closeRate = 0.0143) + public StubSwapDealService(OptUserInfo optUser, IEnumerable omit, double closeRate = 0.0143, + Dictionary market = null) : base(optUser) { _omit = new HashSet(omit.Select(d => d.Date)); _closeRate = closeRate; + _market = market; } protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { rate = 0d; if (underlyingCode != "FR007") return false; - var map = new Dictionary(Fr007Market) { [CloseDate] = _closeRate }; + var map = new Dictionary(_market ?? Fr007Market) { [CloseDate] = _closeRate }; if (_omit.Contains(valueDate.Date)) return false; if (map.TryGetValue(valueDate.Date, out rate)) { @@ -75,7 +83,7 @@ namespace YLErp.Modules.SwapModule private static OptUserInfo MakeOptUser() => new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest); - private static trade BuildTrade(DateTime closeDate) + private static trade BuildTrade(DateTime closeDate, string calcMode = "10") { var extend = new trade_extend { @@ -83,7 +91,7 @@ namespace YLErp.Modules.SwapModule ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = AnnualDays, - InterestCalcMode = "10", + InterestCalcMode = calcMode, SettlementRules = 0 }) }; @@ -102,7 +110,8 @@ namespace YLErp.Modules.SwapModule }; } - private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7) + private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7, + int interestRule = 0) { var intervalModels = new List { @@ -124,7 +133,7 @@ namespace YLErp.Modules.SwapModule InterestType = (int)interestType, IsAnnualized = true, interest_rest_days = restDays, - interest_rule = 0, + interest_rule = interestRule, FloatRateUnderlyingCode = "FR007", FloatRate = 0m, PosiNotionalValue = Notional, @@ -155,16 +164,20 @@ namespace YLErp.Modules.SwapModule eod_swap_position preEod = null, decimal closePrecent = 1m, DateTime? omitDate = null, - double closeRate = 0.0143) + double closeRate = 0.0143, + string calcMode = "10", + int interestRule = 0, + bool newCalcLast = false, + Dictionary market = null) { var cd = closeDate ?? CloseDate; var omit = new HashSet(); if (omitDate.HasValue) omit.Add(omitDate.Value.Date); else if (!includeCloseDate) omit.Add(cd.Date); - var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate); - var td = BuildTrade(cd); - var position = BuildPosition(interestType, cd, restDays); + var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate, market); + var td = BuildTrade(cd, calcMode); + var position = BuildPosition(interestType, cd, restDays, interestRule); var eodList = preEod == null ? new List() : new List { preEod }; @@ -175,7 +188,7 @@ namespace YLErp.Modules.SwapModule new List { position }, Notional, Notional, closePrecent, (int)SwapEventTypeEnum.平仓, false, Notional, - false, settment: false, newCalcLast: false, closeList: null); + false, settment: false, newCalcLast: newCalcLast, closeList: null); Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件"); return new Outcome { Fe = interests[0] }; } @@ -283,5 +296,53 @@ namespace YLErp.Modules.SwapModule AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: true, closeDate: NonIntCloseDate, preEod: BuildPreEod(StartDate)), "单利-非整倍数-有7/20价-应成功"); } + + // ── 算尾守卫(EQD-6968 方案一场景4:算尾+当前营业日+当日重置日+当日缺价 → 必须仍拦截)── + // 放宽只针对"该日利率不参与计息"的场景;算尾时当日利率被消费,缺价拦截是正确依赖,不得误放。 + + [TestMethod] + public void Guard_TailCalced_ResetDayFr007Missing_StillThrows() + { + var o = Run(InterestTypeEnum.复利, includeCloseDate: false, calcMode: "11", + preEod: BuildPreEod(new DateTime(2026, 7, 13))); + Assert.IsTrue(o.Threw, "算尾(11)+当日重置日+当日缺价 → 应拦截(该日利率被消费)"); + StringAssert.Contains(o.Ex.Message, "FR007"); + } + + [TestMethod] + public void Guard_TailCalced_Simple_ResetDayFr007Missing_StillThrows() + { + var o = Run(InterestTypeEnum.单利, includeCloseDate: false, calcMode: "11", + preEod: BuildPreEod(StartDate)); + Assert.IsTrue(o.Threw, "单利 算尾(11)+当日重置日+当日缺价 → 应拦截"); + StringAssert.Contains(o.Ex.Message, "FR007"); + } + + [TestMethod] + public void Baseline_TailCalced_ResetDayFr007Present_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, calcMode: "11", + preEod: BuildPreEod(new DateTime(2026, 7, 13))), + "算尾(11)+当日重置日+当日有价 → 应成功"); + } + + // ── newCalcLast 守卫:交易本身"10"不算尾,但本次平仓显式指定算尾 → effectiveCalcLast=true → 缺价仍拦截 ── + + [TestMethod] + public void Guard_NewCalcLast_OverridesToTail_MissingPrice_StillThrows() + { + var o = Run(InterestTypeEnum.复利, includeCloseDate: false, newCalcLast: true, + preEod: BuildPreEod(new DateTime(2026, 7, 13))); + Assert.IsTrue(o.Threw, "不算尾(10)+本次平仓指定算尾+当日缺价 → 应按算尾拦截"); + StringAssert.Contains(o.Ex.Message, "FR007"); + } + + [TestMethod] + public void Baseline_NewCalcLast_OverridesToTail_WithPrice_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, newCalcLast: true, + preEod: BuildPreEod(new DateTime(2026, 7, 13))), + "不算尾(10)+本次平仓指定算尾+当日有价 → 应成功"); + } } }