diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs index ec4d2d65..a4c65c4b 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs @@ -97,7 +97,7 @@ namespace YLErp.Modules.SwapModule private static OptUserInfo MakeOptUser() => new(0, nameof(GLMS20260817Fr007UnwindMorningTest), OptUserFrom.UnitTest); - private static trade BuildTrade(DateTime closeDate, string calcMode = "10") + private static trade BuildTrade(DateTime closeDate, string calcMode = "10", DateTime? exerciseDate = null) { var extend = new trade_extend { @@ -117,7 +117,7 @@ namespace YLErp.Modules.SwapModule TradeType = "债券TRS", TradeDate = TradeDate, StartDate = StartDate, - ExerciseDate = closeDate.AddDays(1), + ExerciseDate = exerciseDate ?? closeDate.AddDays(1), TradeStatus = "已平仓", ValidState = "Valid", trade_extend = extend @@ -183,7 +183,9 @@ namespace YLErp.Modules.SwapModule int interestRule = 0, bool newCalcLast = false, Dictionary market = null, - DateTime? omitDate2 = null) + DateTime? omitDate2 = null, + bool settment = false, + DateTime? exerciseDate = null) { var cd = closeDate ?? CloseDate; var omit = new HashSet(); @@ -195,7 +197,7 @@ namespace YLErp.Modules.SwapModule else if (!includeCloseDate) omit.Add(cd.Date); var svc = new StubSwapDealService(MakeOptUser(), omit, closeRate, market); - var td = BuildTrade(cd, calcMode); + var td = BuildTrade(cd, calcMode, exerciseDate); var position = BuildPosition(interestType, cd, restDays, interestRule); var eodList = preEod == null ? new List() @@ -207,7 +209,7 @@ namespace YLErp.Modules.SwapModule new List { position }, Notional, Notional, closePrecent, (int)SwapEventTypeEnum.平仓, false, Notional, - false, settment: false, newCalcLast: newCalcLast, closeList: null); + false, settment: settment, newCalcLast: newCalcLast, closeList: null); Assert.AreEqual(1, interests.Count, "应返回恰好 1 条利息事件"); return new Outcome { Fe = interests[0] }; } @@ -421,5 +423,35 @@ namespace YLErp.Modules.SwapModule preEod: BuildPreEod(new DateTime(2026, 7, 13))), "单利 算尾(11)+当日非重置日+当日缺价 → 应放行"); } + + // ── 不算头不算尾(calcMode="00"):CalcFirst=false 组合 ── + // 对尾日 FR007 行为与"10"一致(calcLast 同 false);另以单利精确断言钉 CalcFirst 语义—— + // "00" 比"10"恰好少计开始日一天的利息(单利无基数效应,差值可精确到分毫)。 + + [TestMethod] + public void Red_Compound_NoHeadNoTail_WithoutCloseDateFr007_Succeeds() + { + AssertNoThrow(Run(InterestTypeEnum.复利, includeCloseDate: false, calcMode: "00", + preEod: BuildPreEod(new DateTime(2026, 7, 13))), + "不算头不算尾(00)+缺平仓日价 → 应放行(与10同口径,尾日不参与计息)"); + } + + [TestMethod] + public void CalcFirst_Simple_NoHeadDropsExactlyStartDayInterest() + { + // closeRate=0.0142 与 7/6 定盘相同:使"00"首段种子利率与"10"首段取价利率一致, + // 断言只对"少计开始日一天"敏感,免疫无日终快照时 fetchAfter=interestStart-1 + // 跳过 7/6 取价、首段用种子利率的既有取价细节。 + var w10 = Run(InterestTypeEnum.单利, includeCloseDate: true, calcMode: "10", closeRate: 0.0142); + var w00 = Run(InterestTypeEnum.单利, includeCloseDate: true, calcMode: "00", closeRate: 0.0142); + Assert.IsFalse(w10.Threw, "10 不应抛:" + w10.Ex?.Message); + Assert.IsFalse(w00.Threw, "00 不应抛:" + w00.Ex?.Message); + // 开始日 7/6 属首段:all-in = spread(-0.0155) + 定盘(0.0142) = -0.0013; + // 单利下 00 与 10 的利息差 = 恰好首日一天利息(年化 A365)。 + var expectedStartDayInterest = Notional * (Spread + 0.0142m) / AnnualDays; + Assert.AreEqual(expectedStartDayInterest, w10.Fe.InterestAmount - w00.Fe.InterestAmount, 0.0000001m, + "不算头(00)应恰好少计开始日一天利息(CalcFirst 回归锚)"); + } } } +