From a97476c4daf61d83f84aebfd08d35231bf55aa69 Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 17 Aug 2026 22:21:55 +0800 Subject: [PATCH] =?UTF-8?q?EQD-6968:=20=E4=B8=8D=E7=AE=97=E5=B0=BE?= =?UTF-8?q?=E7=BC=BA=E4=BB=B7=E6=94=B9=E6=B2=BF=E7=94=A8=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E5=88=A9=E7=8E=87(=E5=88=A0=E6=9C=9F=E5=88=9D=E5=9B=9E?= =?UTF-8?q?=E9=80=80)=20+=20=E5=8F=96=E4=BB=B7=E5=86=B3=E7=AD=96FIX?= =?UTF-8?q?=E5=B8=B8=E9=A9=BB=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① GetFloatRate 不算尾缺价不再回退取期初定盘,改沿用已有利率 (preEod.FloatRate/position.FloatRate)——其返回值仅作 preEod.id==0 的种子 且会被分段首段强制取价覆盖,期初取价是无消费者的多余DB读且语义误导; ② BuildSegmentRates 等价重排为 if/else-if 结构,注释改"跳过取价/沿用"准确措辞 (行为不变:排除日有价仍取新利率,缺价沿用上一重置日利率); ③ 防历史bug"取错重置日利率"复发:GetFloatRate/ResolveFloatRate/BuildSegmentRates 三处取价决策经 SwapCalcTrace.Critical 常驻 Info 落盘——重置日判定、取价日 (interest_rule回拨)、命中/缺失、不算尾缺价沿用来源、排除日跳过全程可审计; ④ 测试真内存化:Stub 补 GetConsumedInterest=>0 override(复利路径原偷连96库, 与类注释"内存,不连库"矛盾);一致性断言措辞对齐新语义; ⑤ 修复 SwapDealService.cs 文件头双 BOM 损坏。 验证:GLMS20260817Fr007UnwindMorningTest 12/12 内存全绿; 邻近套件 67过/15败与改动前基线完全一致(15败均为沙箱无96库 Connect Timeout)。 --- .../GLMS20260817Fr007UnwindMorningTest.cs | 10 ++- .../Modules/SwapModule/SwapDealService.cs | 80 ++++++++++++++----- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs index 9823429a..b05558e0 100644 --- a/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs +++ b/UnitTestProject/Modules/SwapModule/GLMS20260817Fr007UnwindMorningTest.cs @@ -11,6 +11,9 @@ namespace YLErp.Modules.SwapModule /// 内存 StubSwapDealService 重写 TryGetFloatRate 按日期返回 FR007(缺失即返回 false → 触发取价失败)。 /// 覆盖两条计息路径(复利 CalcDailyCompoundInterest / 单利 CalcDailySimpleInterest)共用的修复点 BuildSegmentRates, /// 以及全平重放分支、非整倍数边界、数值一致性("跳过取价=沿用上一重置日利率")。 + /// + /// 核心语义:算头不算尾(calcLast=false)时 endDate 当天不计息,其 FR007 利率不参与计息。 + /// 缺价时跳过取价(currentFloat 保持不变),不回退取其他日期利率,不告警。 /// [TestClass] public class GLMS20260817Fr007UnwindMorningTest @@ -57,6 +60,9 @@ namespace YLErp.Modules.SwapModule } return false; } + + // 内存世界无历史结息流水,consumedInterest=0(与本类"内存,不连库"声明一致;否则复利路径偷连 96 库) + public override decimal GetConsumedInterest(int tradeId, long positionId, DateTime beforeDate) => 0m; } private sealed class Outcome @@ -233,7 +239,7 @@ namespace YLErp.Modules.SwapModule 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] @@ -244,7 +250,7 @@ namespace YLErp.Modules.SwapModule 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] diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 62c2f50b..bdc29528 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -853,31 +853,44 @@ namespace YLErp.Modules.SwapModule if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return position.FloatRate; int days = (endDate - startDate).Days; + bool isResetDay = days % period == 0; // 重置日恰为到期日(endDate)时,取价日=endDate;否则=startDate(原逻辑)。 DateTime rateDate = IndexFixerBase.GetFixingDate( - days % period == 0 ? endDate : startDate, position.interest_rule); + isResetDay ? endDate : startDate, position.interest_rule); + // 历史上有"取错重置日利率"的线上 bug,取价决策必须常驻落盘(SwapCalcTrace.Critical 无条件 Info)。 + SwapCalcTrace.Critical( + $"FIX GetFloatRate p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] days={days} period={period} " + + $"重置日={isResetDay} rule={position.interest_rule} 取价日={rateDate:yyyy-MM-dd} calcLast={calcLast} " + + $"preEod={(preEod.id != 0 ? $"{preEod.ValueDate:yyyy-MM-dd}:{preEod.FloatRate:P6}" : "无")}"); - if (preEod.id != 0 && days % period != 0) + if (preEod.id != 0 && !isResetDay) { + SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 非重置日→沿用昨日终FloatRate={preEod.FloatRate:P6}"); position.FloatRate = positionClone.FloatRate = preEod.FloatRate; return preEod.FloatRate; } if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate)) { + SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 重置日→取{rateDate:yyyy-MM-dd}定盘={rate:P6}"); position.FloatRate = positionClone.FloatRate = rate; return position.FloatRate; } if (!swap) { - // EQD-6968:算尾(calcLast=true)时结算日 FR007 必须落表,缺失即依赖报错(既有正确行为); - // 不算尾(calcLast=false)时结算日 FR007 不强制,缺失则回退期初定盘,避免上午未发布误拦平仓。 if (calcLast) + { + SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且算尾→抛异常拦截"); throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{rateDate:yyyy年MM月dd日}的价格"); - if (IndexFixer.TryGetFixing(IndexFixerBase.GetFixingDate(startDate, position.interest_rule), position.FloatRateUnderlyingCode, out decimal fb)) - return fb; - return position.FloatRate; + } + // 算头不算尾(calcLast=false):endDate 当天不计息,其 FR007 利率不参与计息, + // 缺价时直接沿用已有利率,不回退取其他日期利率,不告警。 + var kept = preEod.id != 0 ? preEod.FloatRate : position.FloatRate; + SwapCalcTrace.Critical( + $"FIX GetFloatRate p{position.id} {rateDate:yyyy-MM-dd}缺价且不算尾→沿用已有利率={kept:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})"); + return kept; } + SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 互换事件(swap=true)→利率不参与,返回0"); return 0m; } @@ -1301,7 +1314,19 @@ namespace YLErp.Modules.SwapModule if (string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) return fallback; var fixingDate = IndexFixerBase.GetFixingDate(date, position.interest_rule); if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - return fixing != 0m ? fixing : fallback; + { + if (fixing != 0m) + { + SwapCalcTrace.Critical( + $"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→定盘={fixing:P6}"); + return fixing; + } + SwapCalcTrace.Critical( + $"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd})→定盘=0视为缺价,沿用fallback={fallback:P6}"); + return fallback; + } + SwapCalcTrace.Critical( + $"FIX Resolve p{position.id} {date:yyyy-MM-dd}(取价日={fixingDate:yyyy-MM-dd} rule={position.interest_rule})→缺价,抛异常"); throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); } @@ -1317,26 +1342,37 @@ namespace YLErp.Modules.SwapModule var rates = new List<(DateTime, decimal)>(); var calcDays = (endDate - startDate).Days; decimal currentFloat = initialFloat; + SwapCalcTrace.Critical( + $"FIX Segments p{position.id} [{startDate:yyyy-MM-dd}→{endDate:yyyy-MM-dd}] period={interestPeriod} " + + $"fetchAfter={(fetchAfterDate?.ToString("yyyy-MM-dd") ?? "全程")} calcLast={calcLast} " + + $"排除起点={(relaxedFixingFromDate?.ToString("yyyy-MM-dd") ?? (calcLast ? "无" : endDate.ToString("yyyy-MM-dd")))} seed={initialFloat:P6} spread={spread:P6}"); for (int i = 0; i <= calcDays; i += interestPeriod) { var resetDate = startDate.AddDays(i); bool needFetch = (fetchAfterDate == null || resetDate > fetchAfterDate.Value); - // EQD-6968:不算尾(calcLast=false)时,从真实平仓日(relaxedFixingFromDate,缺省取 endDate)起的重置 - // 优先取新利率(满足 GLMS-JIATT-20260805 重置日=平仓日用新利率);缺失则沿用上一重置利率, - // 不抛异常,以免上午 FR007 未发布误拦平仓。算尾(calcLast=true)时尾日定盘照常强制。 - bool relaxEndFixing = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate); - if (needFetch) + // 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息。 + // 从 relaxedFixingFromDate(缺省取 endDate)起的重置日:有价则取(满足重置日=平仓日用新利率), + // 缺价则跳过取价——currentFloat 保持不变(沿用上一重置日利率),不回退、不告警、不抛异常。 + // 算尾(calcLast=true)时所有重置日定盘照常强制取价。 + bool isExcludedEnd = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate); + if (needFetch && !isExcludedEnd) { - if (!relaxEndFixing) + currentFloat = ResolveFloatRate(position, resetDate, currentFloat); + } + else if (needFetch && isExcludedEnd) + { + // 有价则取新利率(重置日=平仓日时仍优先使用新利率);无价则跳过——该日利率不参与计息 + var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule); + if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r)) { - currentFloat = ResolveFloatRate(position, resetDate, currentFloat); + currentFloat = r; + SwapCalcTrace.Critical( + $"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日有价→取新定盘(取价日={fixingDate:yyyy-MM-dd})={r:P6}"); } else { - var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r)) - currentFloat = r; - // 否则沿用 currentFloat(上一重置日利率) + SwapCalcTrace.Critical( + $"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日缺价→跳过取价,沿用={currentFloat:P6}(该日不计息)"); } } rates.Add((resetDate, spread + currentFloat)); @@ -1363,8 +1399,8 @@ namespace YLErp.Modules.SwapModule int interestPeriod = position.interest_rest_days ?? 1; // 分段取率:复利全程重放,每个重置日(含 startDate)取 FR007(fetchAfterDate=null)。 - // EQD-6968:calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移; - // relaxedFixingFromDate 仅重放补计不算尾漏计利息时非 null(=真实平仓日),锚定尾日定盘放松点。 + // calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移; + // relaxedFixingFromDate 仅重放补计不算尾漏计利息时非 null(=真实平仓日),锚定尾日跳过取价起点。 var (segmentRates, currentFloat) = BuildSegmentRates( startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate, fetchAfterDate: null, calcLast: calcLast, relaxedFixingFromDate: relaxedFixingFromDate); @@ -1407,7 +1443,7 @@ namespace YLErp.Modules.SwapModule var accrualBasis = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; // 分段取率:仅 ValueDate 之后的重置日才取 FR007(fetchAfterDate=ValueDate)。 - // EQD-6968:calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移。 + // calcLast 透传,与下方 AccruePeriod 的边界同源,避免两处漂移。 var (segmentRates, currentFloat) = BuildSegmentRates( startDate, endDate, interestPeriod, position, flowEvent.InterestRate, floateRate, fetchAfterDate: preEodPosition.ValueDate, calcLast: calcLast, relaxedFixingFromDate: relaxedFixingFromDate);