From b01b485ee4e7451d20d322d425a321c9c298f20f Mon Sep 17 00:00:00 2001 From: hjhan Date: Fri, 14 Aug 2026 15:52:52 +0800 Subject: [PATCH] =?UTF-8?q?test(swap):=20GetInterests=20=E5=8F=8C=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E8=AF=AD=E4=B9=89=E5=AD=97=E7=AC=A6=E5=8C=96=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E2=80=94=E2=80=94=E5=B9=B6=E5=AE=9E=E6=B5=8B=E5=8F=91?= =?UTF-8?q?=E7=8E=B0=E5=A4=8D=E5=88=A9=E5=8F=A3=E5=BE=84=E5=88=86=E6=AD=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 钉住三组现状(Step3 特判降级的前置回归网): - 复利×mode2×部分平仓30%:盘中=0.036164835616(closePosi=平掉额300 全程重放); EOD=0.059041913305(preEod待实现0.05 + 平掉额末段增量0.009042,closePercent==1 分支) ⚠️ 两值不等=同一经济事件两种结息额的口径分歧,已留档待业务裁决(勿当既定正确) - 单利×mode2:双入口数值留档(Console),断言非零 - 复利×mode9 全平(posi=0):兜底覆盖生效,结息额非零(兜底钉子) 基建复用 GetInterestsUnitTest_T0 口径(T+0、4/27起息、11算头算尾、FR007内存取价stub)。 --- .../GetInterestsEntrySemanticsTest.cs | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 UnitTestProject/Modules/SwapModule/GetInterestsEntrySemanticsTest.cs diff --git a/UnitTestProject/Modules/SwapModule/GetInterestsEntrySemanticsTest.cs b/UnitTestProject/Modules/SwapModule/GetInterestsEntrySemanticsTest.cs new file mode 100644 index 00000000..7cf471e0 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/GetInterestsEntrySemanticsTest.cs @@ -0,0 +1,220 @@ +using Newtonsoft.Json; +using YLErp.DBModels.Enums; + +namespace YLErp.Modules.SwapModule +{ + /// + /// GetInterests 双显式入口语义字符化测试(Step3"特判降级"的前置钉子)。 + /// + /// 背景:GetIntradayUnwindInterests(盘中:平仓前剩余×实际比例)与 + /// CalcEodPostCloseSettleInterests(EOD平仓后收盘:平仓后剩余×恒1)是同一经济事件 + /// (部分平仓)的两套传参语义,靠 GetInterests 内 mode2 无条件覆盖 / mode9 全平兜底粘合。 + /// 本测试钉死当前行为,使后续特判降级/语义重构有回归网: + /// ① 复利×mode2:closePrincipal(特判产物)是 CalcDailyCompoundInterest 的重放本金—— + /// 两入口 closePosiNotionalValue 均为实际平掉额 → InterestAmount 必须相等; + /// ② 单利×mode2:CalcDailySimpleInterest 消费的是 posiPrincipal×closePercent—— + /// 盘中(平仓前×比例) vs EOD(剩余×1) 数值口径可能不同,本测试【记录现状】(见各断言注释); + /// ③ mode9 全平(posi=0):兜底覆盖生效,结息额非零。 + /// + /// 数据基建复用 GetInterestsUnitTest_T0 的构建器口径(T+0,4/27起息,"11"算头算尾)。 + /// + [TestClass] + public class GetInterestsEntrySemanticsTest + { + private const decimal Principal = 1000m; + private const decimal FixedRate = 0.01m; + private const decimal FloatRate = 0.001m; + private const int AnnualDays = 365; + private const int ResetPeriod = 3; + + private static readonly DateTime TradeDate = new(2026, 4, 27); + private static readonly DateTime StartDate = new(2026, 4, 27); + private static readonly DateTime ExerciseDate = new(2027, 4, 27); + private static readonly DateTime UnwindDate = new(2026, 4, 30); + + // 平仓前剩余 1000,平掉 30%(300),收盘后剩余 700 + private const decimal PreClose = 1000m; + private const decimal Closed = 300m; + private const decimal Remaining = 700m; + private const decimal ClosePercent = 0.3m; + + #region Stub(浮动利率内存取价,与 T0 同款) + + private sealed class StubSwapDealService : SwapDealService + { + private readonly IReadOnlyDictionary _floatRates; + public StubSwapDealService(OptUserInfo optUser, IReadOnlyDictionary floatRates) : base(optUser) + { + _floatRates = floatRates; + } + protected override bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) + { + if (!string.Equals(underlyingCode, "FR007", StringComparison.OrdinalIgnoreCase)) { rate = 0; return false; } + if (_floatRates.TryGetValue(valueDate.Date, out rate)) return true; + rate = 0; + return false; + } + } + + private static SwapDealService CreateService() => new StubSwapDealService( + new OptUserInfo(0, nameof(GetInterestsEntrySemanticsTest), OptUserFrom.UnitTest), + new Dictionary + { + [new DateTime(2026, 4, 27)] = (double)FloatRate, + [new DateTime(2026, 4, 28)] = (double)FloatRate, + [new DateTime(2026, 4, 29)] = (double)FloatRate, + [new DateTime(2026, 4, 30)] = (double)FloatRate, + }); + + #endregion + + #region 数据构建(T0 口径) + + private static trade CreateTrade() + { + var extend = new trade_extend + { + TradeId = 1, + ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson + { + AnnualDays = AnnualDays, + InterestCalcMode = "11", // 算头算尾 + SettlementRules = 0 + }) + }; + return new trade + { + id = 1, TradeNumber = "UT-INT-ENTRY-SEMANTICS", ClientId = 999998, + TradeType = "收益互换", TradeDate = TradeDate, StartDate = StartDate, + ExerciseDate = ExerciseDate, TradeStatus = "确认成交", ValidState = "Valid", + trade_extend = extend + }; + } + + private static swap_position CreatePosition(InterestModeEnum mode, InterestTypeEnum interestType, bool floating = false) + { + var intervalModels = new List + { + new IntervalModel { Date = ExerciseDate, Rate = FixedRate, Settlement = 0 } + }; + return new swap_position + { + id = 1001, SwapTradeId = 1, PositionType = (int)PositionTypeFlag.Unknown, + InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)mode, + InterestRateDefault = FixedRate, InterestPrincipalFix = Principal, + PosiStartDate = StartDate, PosiMatuirityDate = ExerciseDate, + IsInitial = true, Invalid = false, InterestType = (int)interestType, + IsAnnualized = true, interest_rest_days = ResetPeriod, interest_rule = 0, + FloatRateUnderlyingCode = floating ? "FR007" : null, + InterestSwapInterval = JsonConvert.SerializeObject(intervalModels) + }; + } + + private static eod_swap_position CreatePreEod(decimal interestSum, decimal principal) + => new() + { + id = 1, SwapTradeId = 1, PositionId = 1001, ValueDate = new DateTime(2026, 4, 29), + ClientId = 999998, FloatRate = FloatRate, TdInterestPrincipal = principal, + PosiNotionalValue = principal, InterestIncomeSum = interestSum, InterestProfitSum = interestSum + }; + + #endregion + + /// + /// 复利×mode2×部分平仓30%:钉住两入口【当前】结息口径(2026-08-14 实测,字符化)。 + /// + /// 实测(closePrincipal 特判两边均=平掉额300,但消费路径不同): + /// 盘中 = 0.036164835616 —— CalcDailyCompoundInterest 以 closePosi(300) 全程重放 [4/27,4/30]; + /// EOD = 0.059041913305 —— InitSwapDealInterest closePercent==1 分支: + /// preEod.InterestIncomeSum(0.05 全腿待实现) + amountAtEnd(0.036165) - amountAtPrevEod(0.027123)。 + /// + /// ⚠️ 两值不等 = 已观察到的口径分歧(同一经济事件两种结息额),非断言失败项; + /// 待业务裁决哪个口径正确前,本测试锁死两值防意外漂移。裁决后改断言为"相等"或删除错方。 + /// + [TestMethod] + public void 复利_mode2_部分平仓_双入口口径钉住现状() + { + var td = CreateTrade(); + var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.复利, floating: true); + var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose); + var eodPositions = new List { preEod }; + var positions = new List { position }; + + var intraday = CreateService().GetIntradayUnwindInterests(td, td.trade_extend, UnwindDate, UnwindDate, + eodPositions, positions, PreClose, PreClose, 0m, Closed, ClosePercent, + (int)SwapEventTypeEnum.平仓, tdClose: true, orginPv: PreClose, add: true, newCalcLast: false, closeList: null); + + var eodPostClose = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate, + eodPositions, positions, Remaining, Remaining, 0m, Closed, 1m, + (int)SwapEventTypeEnum.平仓, tdClose: false, needPrice: true, grossPrice: 1m, orginPv: PreClose, + add: true, settment: false, newCalcLast: false, closeList: null); + + Assert.AreEqual(1, intraday.Count); + Assert.AreEqual(1, eodPostClose.Count); + Console.WriteLine($"[复利mode2] 盘中 InterestAmount={intraday[0].InterestAmount} / EOD={eodPostClose[0].InterestAmount}"); + + // 钉住两入口各自的当前值(容差 1e-9 级,防任何实现漂移) + Assert.AreEqual(0.036164835616m, intraday[0].InterestAmount, 0.000000001m, + "盘中口径:closePosi(平掉额300) 全程重放利息。此值变化=盘中复利口径漂移"); + Assert.AreEqual(0.059041913305m, eodPostClose[0].InterestAmount, 0.000000001m, + "EOD口径:preEod待实现(0.05) + 平掉额末段增量(0.009042)。此值变化=EOD平仓后收盘复利口径漂移"); + } + + /// + /// 单利×mode2×部分平仓30%:记录两入口当前口径(快照×比例 vs 重放基数差异面)。 + /// 单利消费 posiPrincipal×closePercent:盘中 1000×0.3 vs EOD 700×1 —— 若两值不等, + /// 这是当前系统的已知口径差异面(非断言失败项),数值以 Console 留档,供特判降级时对照。 + /// + [TestMethod] + public void 单利_mode2_部分平仓_双入口口径留档() + { + var td = CreateTrade(); + var position = CreatePosition(InterestModeEnum.合约名义本金规模, InterestTypeEnum.单利); + var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose); + var eodPositions = new List { preEod }; + var positions = new List { position }; + + var intraday = CreateService().GetIntradayUnwindInterests(td, td.trade_extend, UnwindDate, UnwindDate, + eodPositions, positions, PreClose, PreClose, 0m, Closed, ClosePercent, + (int)SwapEventTypeEnum.平仓, tdClose: true, orginPv: PreClose, add: true, newCalcLast: false, closeList: null); + + var eodPostClose = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate, + eodPositions, positions, Remaining, Remaining, 0m, Closed, 1m, + (int)SwapEventTypeEnum.平仓, tdClose: false, needPrice: true, grossPrice: 1m, orginPv: PreClose, + add: true, settment: false, newCalcLast: false, closeList: null); + + Assert.AreEqual(1, intraday.Count); + Assert.AreEqual(1, eodPostClose.Count); + Console.WriteLine($"[单利mode2] 盘中 InterestAmount={intraday[0].InterestAmount} / EOD={eodPostClose[0].InterestAmount}"); + Console.WriteLine($"[单利mode2] TdInterestAmount: 盘中={intraday[0].TdInterestAmount} / EOD={eodPostClose[0].TdInterestAmount}"); + // 钉住"两入口非零"这一最低限度事实;数值差异本身是记录项,不是失败项 + Assert.IsTrue(intraday[0].InterestAmount != 0m, "盘中单利结息额不应为0"); + Assert.IsTrue(eodPostClose[0].InterestAmount != 0m, "EOD单利结息额不应为0"); + } + + /// + /// mode9 全平(EOD,posi=0):特判兜底触发 closePrincipal=closePosiNotionalValue(实际平掉额), + /// 结息额非零。若兜底被删,closePrincipal=0×1=0 → 结息额归零 → 本断言红。 + /// + [TestMethod] + public void 复利_mode9_全平_兜底覆盖生效结息额非零() + { + var td = CreateTrade(); + var position = CreatePosition(InterestModeEnum.标的期初全价, InterestTypeEnum.复利, floating: true); + var preEod = CreatePreEod(interestSum: 0.05m, principal: PreClose); + var eodPositions = new List { preEod }; + var positions = new List { position }; + + // 全平:剩余=0,平掉=全部 1000 + var result = CreateService().GetInterests(td, td.trade_extend, UnwindDate, UnwindDate, + eodPositions, positions, 0m, 0m, 0m, PreClose, 1m, + (int)SwapEventTypeEnum.平仓, tdClose: false, needPrice: true, grossPrice: 1m, orginPv: PreClose, + add: true, settment: false, newCalcLast: false, closeList: null); + + Assert.AreEqual(1, result.Count); + Console.WriteLine($"[复利mode9全平] InterestAmount={result[0].InterestAmount}"); + Assert.IsTrue(result[0].InterestAmount != 0m, + "mode9 全平时 posi=0,兜底必须以 closePosiNotionalValue(实际平掉额) 为结息本金,结息额非零(兜底钉子)"); + } + } +}