From 305f8a0322baf1e378e8144a92294620b4bd18cb Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 17 Aug 2026 11:42:41 +0800 Subject: [PATCH] =?UTF-8?q?EQD-6968:=20=E8=A1=A5=E5=BC=BA=20FR007=20?= =?UTF-8?q?=E4=B8=8D=E7=AE=97=E5=B0=BE=E5=B9=B3=E4=BB=93=E8=AF=AF=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E5=9B=9E=E5=BD=92=E5=A5=97=E4=BB=B6=EF=BC=88=E5=8D=95?= =?UTF-8?q?=E5=88=A9Red/=E6=95=B0=E5=80=BC=E4=B8=80=E8=87=B4=E6=80=A7/?= =?UTF-8?q?=E9=9D=9E=E6=95=B4=E5=80=8D=E6=95=B0=E8=BE=B9=E7=95=8C=EF=BC=8C?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=B8=BA=20Run=20=E8=BF=90=E8=A1=8C=E5=99=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GLMS20260817Fr007UnwindMorningTest.cs | 301 ++++++++++-------- 1 file changed, 173 insertions(+), 128 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs index 54cf7baa..9823429a 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs @@ -4,44 +4,24 @@ using YLErp.DBModels.Enums; namespace YLErp.Modules.SwapModule { /// - /// EQD-6968 FR007 不算尾平仓"上午未发布"误拦截 - RED 复现,修复后转绿(内存,不连库) + /// EQD-6968 FR007 不算尾平仓"上午未发布"误拦截 —— 修复后回归套件(内存,不连库)。 /// 任务编号 EQD-6968;现象报告日 2026-08-17。参照 GLMS20260703CloseInterestTest 内存 FR007 写法。 + /// + /// 设计:所有场景经单一 Run 运行器驱动真实 GetInterests 平仓利息路径; + /// 内存 StubSwapDealService 重写 TryGetFloatRate 按日期返回 FR007(缺失即返回 false → 触发取价失败)。 + /// 覆盖两条计息路径(复利 CalcDailyCompoundInterest / 单利 CalcDailySimpleInterest)共用的修复点 BuildSegmentRates, + /// 以及全平重放分支、非整倍数边界、数值一致性("跳过取价=沿用上一重置日利率")。 /// [TestClass] public class GLMS20260817Fr007UnwindMorningTest { - private sealed class StubSwapDealService : SwapDealService + private static readonly Dictionary Fr007Market = new() { - private readonly bool _includeCloseDate; - - public StubSwapDealService(OptUserInfo optUser, bool includeCloseDate) : base(optUser) - { - _includeCloseDate = includeCloseDate; - } - - public readonly List<(DateTime RequestDate, double Rate)> FloatRateCalls = new(); - - protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) - { - rate = 0d; - if (underlyingCode != "FR007") return false; - - var fr007 = new Dictionary - { - [new DateTime(2026, 7, 6)] = 0.0142, - [new DateTime(2026, 7, 13)] = 0.01425, - }; - if (_includeCloseDate) - fr007[new DateTime(2026, 7, 20)] = 0.0143; - - if (fr007.TryGetValue(valueDate.Date, out rate)) - { - FloatRateCalls.Add((valueDate.Date, rate)); - return true; - } - return false; - } - } + [new DateTime(2026, 7, 6)] = 0.0142, + [new DateTime(2026, 7, 13)] = 0.01425, + [new DateTime(2026, 7, 20)] = 0.0143, + }; + private const double PreviousResetRate = 0.01425; private const decimal Notional = 279486108.21m; private const int AnnualDays = 365; @@ -49,15 +29,47 @@ namespace YLErp.Modules.SwapModule private static readonly DateTime StartDate = new(2026, 7, 6); private static readonly DateTime TradeDate = new(2026, 7, 3); private static readonly DateTime CloseDate = new(2026, 7, 20); + private static readonly DateTime NonIntCloseDate = new(2026, 7, 22); - private SwapDealService MakeService(bool includeCloseDate) + private sealed class StubSwapDealService : SwapDealService { - return new StubSwapDealService( - new OptUserInfo(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest), - includeCloseDate); + private readonly HashSet _omit; + private readonly double _closeRate; + public readonly List PricedDates = new(); + + public StubSwapDealService(OptUserInfo optUser, IEnumerable omit, double closeRate = 0.0143) + : base(optUser) + { + _omit = new HashSet(omit.Select(d => d.Date)); + _closeRate = closeRate; + } + + 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 }; + if (_omit.Contains(valueDate.Date)) return false; + if (map.TryGetValue(valueDate.Date, out rate)) + { + PricedDates.Add(valueDate.Date); + return true; + } + return false; + } } - private static trade CreateTrade() + private sealed class Outcome + { + public swap_flow_event Fe; + public Exception Ex; + public bool Threw => Ex != null; + } + + private static OptUserInfo MakeOptUser() => + new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest); + + private static trade BuildTrade(DateTime closeDate) { var extend = new trade_extend { @@ -77,18 +89,18 @@ namespace YLErp.Modules.SwapModule TradeType = "债券TRS", TradeDate = TradeDate, StartDate = StartDate, - ExerciseDate = CloseDate.AddDays(1), + ExerciseDate = closeDate.AddDays(1), TradeStatus = "已平仓", ValidState = "Valid", trade_extend = extend }; } - private static swap_position CreateBondPosition(InterestTypeEnum interestType, int interestRule = 0) + private static swap_position BuildPosition(InterestTypeEnum interestType, DateTime closeDate, int restDays = 7) { var intervalModels = new List { - new IntervalModel { Date = CloseDate, Rate = Spread, Settlement = 0 } + new IntervalModel { Date = closeDate, Rate = Spread, Settlement = 0 } }; return new swap_position { @@ -100,13 +112,13 @@ namespace YLErp.Modules.SwapModule InterestRateDefault = Spread, InterestPrincipalFix = Notional, PosiStartDate = StartDate, - PosiMatuirityDate = CloseDate, + PosiMatuirityDate = closeDate, IsInitial = true, Invalid = false, InterestType = (int)interestType, IsAnnualized = true, - interest_rest_days = 7, - interest_rule = interestRule, + interest_rest_days = restDays, + interest_rule = 0, FloatRateUnderlyingCode = "FR007", FloatRate = 0m, PosiNotionalValue = Notional, @@ -115,31 +127,13 @@ namespace YLErp.Modules.SwapModule }; } - private swap_flow_event CalcCloseInterest(SwapDealService svc, InterestTypeEnum interestType, int interestRule = 0) - { - var td = CreateTrade(); - var position = CreateBondPosition(interestType, interestRule); - var interests = svc.GetInterests( - td, td.trade_extend, - CloseDate, CloseDate, - new List(), - new List { position }, - Notional, Notional, - 1m, - (int)SwapEventTypeEnum.平仓, - false, Notional, - false, settment: false, newCalcLast: false, closeList: null); - Assert.AreEqual(1, interests.Count); - return interests[0]; - } - - private static eod_swap_position CreatePreEod() + private static eod_swap_position BuildPreEod(DateTime valueDate) { return new eod_swap_position { id = 5001, PositionId = 1001, - ValueDate = new DateTime(2026, 7, 13), + ValueDate = valueDate, FloatRate = 0.01425m, InterestProfitSum = -100000m, TdInterestPrincipal = Notional, @@ -147,90 +141,141 @@ namespace YLErp.Modules.SwapModule }; } - private swap_flow_event CalcCloseInterestEod(SwapDealService svc, InterestTypeEnum interestType, List eodPositions) + private static Outcome Run( + InterestTypeEnum interestType, + bool includeCloseDate, + DateTime? closeDate = null, + int restDays = 7, + eod_swap_position preEod = null, + decimal closePrecent = 1m, + DateTime? omitDate = null, + double closeRate = 0.0143) { - var td = CreateTrade(); - var position = CreateBondPosition(interestType); - var interests = svc.GetInterests( - td, td.trade_extend, - CloseDate, CloseDate, - eodPositions, - new List { position }, - Notional, Notional, - 1m, - (int)SwapEventTypeEnum.平仓, - false, Notional, - false, settment: false, newCalcLast: false, closeList: null); - Assert.AreEqual(1, interests.Count); - return interests[0]; - } + var cd = closeDate ?? CloseDate; + var omit = new HashSet(); + if (omitDate.HasValue) omit.Add(omitDate.Value.Date); + else if (!includeCloseDate) omit.Add(cd.Date); - /// - /// [RED] 不算尾平仓,平仓日 FR007 未发布(内存缺失)→ 当前抛"获取不到FR007...价格"。 - /// 期望:修复后应成功返回(不抛)。当前为 RED(测试失败)。 - /// - [TestMethod] - public void Red_UnwindMorning_WithoutCloseDateFr007_ShouldSucceed() - { - var svc = MakeService(includeCloseDate: false); + var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate); + var td = BuildTrade(cd); + var position = BuildPosition(interestType, cd, restDays); + var eodList = preEod == null + ? new List() + : new List { preEod }; try { - var fe = CalcCloseInterest(svc, InterestTypeEnum.复利); - Assert.IsNotNull(fe); - Assert.IsFalse(fe.InterestAmount == 0 && fe.FloatRate == 0, "返回的利息不应全为零"); + var interests = svc.GetInterests( + td, td.trade_extend, cd, cd, eodList, + new List { position }, + Notional, Notional, closePrecent, + (int)SwapEventTypeEnum.平仓, false, Notional, + false, settment: false, newCalcLast: false, closeList: null); + Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件"); + return new Outcome { Fe = interests[0] }; } catch (Exception ex) { - StringAssert.Contains(ex.Message, "FR007"); - Assert.Fail($"RED 复现成功:不算尾平仓因平仓日 FR007 未发布被误拦截 —— {ex.Message}"); + return new Outcome { Ex = ex }; } } - /// - /// [Baseline] 同场景但提供平仓日 07-20 的 FR007 → 应成功(绿),隔离 stub/路径问题。 - /// - [TestMethod] - public void Baseline_WithCloseDateFr007_Succeeds() + private static void AssertNoThrow(Outcome o, string scenario) { - var svc = MakeService(includeCloseDate: true); - var fe = CalcCloseInterest(svc, InterestTypeEnum.复利); - Assert.IsNotNull(fe); - Assert.IsTrue(fe.InterestAmount != 0, "提供末日 FR007 时应正常算出利息"); + Assert.IsFalse(o.Threw, scenario + " 不应因平仓日 FR007 未发布而抛异常:" + o.Ex?.Message); + Assert.IsNotNull(o.Fe, scenario + " 应返回利息事件"); + Assert.IsFalse(o.Fe.InterestAmount == 0 && o.Fe.FloatRate == 0, scenario + " 利息不应全为零"); } - /// - /// [RED-重放] 不算尾全平(带前日日终持仓 → 走重放分支 replayEndDate=平仓日+1),平仓日 FR007 未发布 → 当前抛。 - /// 修复后(BuildSegmentRates 用 exclusionEndDate 排除真实平仓日)应成功,平仓日用上一重置日利率。 - /// [TestMethod] - public void Red_FullClose_WithPreEod_WithoutCloseDateFr007_Succeeds() + public void Red_Compound_WithoutCloseDateFr007_Succeeds() { - var svc = MakeService(includeCloseDate: false); - var preEod = CreatePreEod(); - try - { - var fe = CalcCloseInterestEod(svc, InterestTypeEnum.复利, new List { preEod }); - Assert.IsNotNull(fe); - Assert.IsFalse(fe.InterestAmount == 0 && fe.FloatRate == 0, "返回的利息不应全为零"); - } - catch (Exception ex) - { - StringAssert.Contains(ex.Message, "FR007"); - Assert.Fail($"RED-重放 复现成功:不算尾全平因平仓日 FR007 未发布被误拦截 —— {ex.Message}"); - } + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: false), "复利-无preEod-缺平仓日"); } - /// - /// [Baseline-重放] 同场景但提供平仓日 07-20 FR007 → 应成功(绿),隔离 stub/路径问题。 - /// [TestMethod] - public void Baseline_FullClose_WithPreEod_WithCloseDateFr007_Succeeds() + public void Baseline_Compound_WithCloseDateFr007_Succeeds() { - var svc = MakeService(includeCloseDate: true); - var preEod = CreatePreEod(); - var fe = CalcCloseInterestEod(svc, InterestTypeEnum.复利, new List { preEod }); - Assert.IsNotNull(fe); - Assert.IsTrue(fe.InterestAmount != 0, "提供末日 FR007 时应正常算出利息"); + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true), "复利-无preEod-有平仓日"); + } + + [TestMethod] + public void Red_Compound_FullClose_WithPreEod_WithoutCloseDateFr007_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: false, preEod: BuildPreEod(new DateTime(2026, 7, 13))), + "复利-全平重放-缺平仓日"); + } + + [TestMethod] + public void Baseline_Compound_FullClose_WithPreEod_WithCloseDateFr007_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, preEod: BuildPreEod(new DateTime(2026, 7, 13))), + "复利-全平重放-有平仓日"); + } + + [TestMethod] + public void Red_Simple_WithoutPreEod_WithoutCloseDateFr007_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: false), "单利-无preEod-缺平仓日"); + } + + [TestMethod] + public void Red_Simple_WithPreEod_WithoutCloseDateFr007_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: false, preEod: BuildPreEod(StartDate)), + "单利-带preEod-缺平仓日"); + } + + [TestMethod] + public void Consistency_Compound_SkipEqualsPreviousRate() + { + var worldA = Run(InterestTypeEnum.复利, includeCloseDate: false); + var worldB = Run(InterestTypeEnum.复利, includeCloseDate: true, closeRate: PreviousResetRate); + Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message); + Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message); + Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount, + "缺价回退世界 应与 显式置上一期利率世界 利息完全一致(钉死=沿用上期)"); + } + + [TestMethod] + public void Consistency_Simple_SkipEqualsPreviousRate() + { + var worldA = Run(InterestTypeEnum.单利, includeCloseDate: false, preEod: BuildPreEod(StartDate)); + var worldB = Run(InterestTypeEnum.单利, includeCloseDate: true, preEod: BuildPreEod(StartDate), closeRate: PreviousResetRate); + Assert.IsFalse(worldA.Threw, "世界A 不应抛:" + worldA.Ex?.Message); + Assert.IsFalse(worldB.Threw, "世界B 不应抛:" + worldB.Ex?.Message); + Assert.AreEqual(worldA.Fe.InterestAmount, worldB.Fe.InterestAmount, + "单利:缺价回退世界 应与 显式置上一期利率世界 利息完全一致"); + } + + [TestMethod] + public void Boundary_Compound_NonIntegerMultiple_LastResetStillPrices() + { + var o = Run(InterestTypeEnum.复利, includeCloseDate: false, closeDate: NonIntCloseDate, omitDate: new DateTime(2026, 7, 20)); + Assert.IsTrue(o.Threw, "非整倍数时末段重置日 7/20 缺价应抛异常(该日利率被消费)"); + StringAssert.Contains(o.Ex.Message, "FR007"); + } + + [TestMethod] + public void Boundary_Compound_NonIntegerMultiple_WithCloseDateSucceeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: true, closeDate: NonIntCloseDate), + "非整倍数-有7/20价-应成功"); + } + + [TestMethod] + public void Boundary_Simple_NonIntegerMultiple_LastResetStillPrices() + { + var o = Run(InterestTypeEnum.单利, includeCloseDate: false, closeDate: NonIntCloseDate, + preEod: BuildPreEod(StartDate), omitDate: new DateTime(2026, 7, 20)); + Assert.IsTrue(o.Threw, "单利 非整倍数时末段重置日 7/20 缺价应抛异常"); + StringAssert.Contains(o.Ex.Message, "FR007"); + } + + [TestMethod] + public void Boundary_Simple_NonIntegerMultiple_WithCloseDateSucceeds() + { + AssertNoThrow(Run(InterestTypeEnum.单利, includeCloseDate: true, closeDate: NonIntCloseDate, preEod: BuildPreEod(StartDate)), + "单利-非整倍数-有7/20价-应成功"); } } }