From fafae5cac0c8a9fa44b7d15a2e38efab1ed3c0b4 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 19 Aug 2026 11:06:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor(swap):=20EQD-6968=20=E5=8F=96=E4=BB=B7?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E8=87=AA=E6=B4=BD=E5=8C=96=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E6=97=A5=E7=BA=AF=E8=B7=B3=E8=BF=87+?= =?UTF-8?q?=E6=8C=81=E4=BB=93=E5=BB=B6=E7=BB=AD=E9=87=8D=E7=BD=AE=E6=97=A5?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E5=86=8D=E5=AE=9A=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 取价规则只由交易属性决定(算不算尾/interest_rule/重置日历),平仓只切窗口: ① BuildSegmentRates 排除日(不算尾尾日)一概不取价(有价也不取)——事件利率恒为 末段已消费利率,与金额同源、与平仓时刻无关(旧口径下午平仓落库新利率,金额 却用旧利率,且上午/下午记录不同);金额零变更(排除日本就零消费)。 ② GetFloatRate 不算尾重置日不再先试取尾日定盘(原'有价则取'分支删除)。 ③ 剩余持仓的新周期利率改由 SwapEodPositionService 显式获取:平仓日恰为重置日 且剩余>0 时,快照 FloatRate 经 ResolveOngoingResetFixing 再定盘为当日新定盘 (生产 GetFixingOrThrow,缺价抛——与 ByEod 重置日/EodCheckSettlePrice 同口径; 原'有价则取'顺带承担此载体职责,现显式分离)。全平/算尾/观察日无需再定盘。 ④ 无日终快照播种 priorValueDate 恒=开始日-1:首重置日(=开始日)的定盘覆盖 [开始日,下一重置日)全部计息日(不算头时 7/7 起仍属首段),必须落在取价窗内。 原仅算头回拨一天,不算头时首段误用种子利率(靠②的尾日取价回填掩盖,现已根治)。 '不算头少计一天'仍由计息边界 IncludeStart=false 承担,天数不变、利率归位。 FIX 日志同步:排除日不取价/EodCloseRefix 再定盘 全程可审计。 验证:全量 976 例 145 败与改动前基线失败名单 diff=0;内存套件(含 ContractReferenceOracle 金标准/InterestEodTailSnapshot)全绿 --- .../Modules/SwapModule/SwapDealService.cs | 48 ++++++++++--------- .../SwapModule/SwapEodPositionService.cs | 22 +++++++++ 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7fd0cc90..192b042a 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -850,8 +850,9 @@ namespace YLErp.Modules.SwapModule /// EQD-6968 取价依赖三判定之一。锚点口径注记:GetFloatRate 传交易起始日 td.StartDate, /// 分段重放/当日归周期判定传 position.PosiStartDate——非初始持仓(部分平仓剩余仓)两锚点可能不同, /// 本方法只收口公式、不统一锚点(统一属行为变更,需业务定调)。 + /// SwapEodPositionService 的"持仓延续腿重置日再定盘"亦用本判定(td.StartDate 锚点)。 /// - private static bool IsResetDay(DateTime date, DateTime anchorDate, int period) + internal static bool IsResetDay(DateTime date, DateTime anchorDate, int period) => (date - anchorDate).Days % period == 0; /// @@ -879,6 +880,17 @@ namespace YLErp.Modules.SwapModule return preEod.FloatRate; } + // EQD-6968 口径自洽化:不算尾(calcLast=false)时 endDate 当天不计息,其定盘一概不取 + // (有价也不取)——事件/回写利率与金额同源(末段已消费利率),杜绝"上午/下午落库利率不同"。 + // 剩余持仓的新周期利率由 SwapEodPositionService 的"重置日再定盘"显式获取,不靠此处顺带。 + if (!swap && !calcLast && isResetDay) + { + var keptNoFetch = preEod.id != 0 ? preEod.FloatRate : position.FloatRate; + SwapCalcTrace.Critical( + $"FIX GetFloatRate p{position.id} 不算尾重置日→不取尾日定盘,沿用已有利率={keptNoFetch:P6}(来源={(preEod.id != 0 ? "preEod.FloatRate" : "position.FloatRate")})"); + return keptNoFetch; + } + if (IndexFixer.TryGetFixing(rateDate, position.FloatRateUnderlyingCode, out decimal rate)) { SwapCalcTrace.Critical($"FIX GetFloatRate p{position.id} 重置日→取{rateDate:yyyy-MM-dd}定盘={rate:P6}"); @@ -1115,11 +1127,12 @@ namespace YLErp.Modules.SwapModule preEod.FloatRate = floatRate; preEod.TdInterestPrincipal = posiPrincipal; preEod.PosiNotionalValue = posiPrincipal; - preEod.ValueDate = td.StartDate.Value; - if (calcFirst) - { - preEod.ValueDate = preEod.ValueDate.AddDays(-1); - } + // priorValueDate 恒取 开始日-1:首个重置日(=开始日)的定盘覆盖 [开始日,下一重置日) 全部计息日 + //(不算头时 7/7 起的计息日仍属首段),必须落在取价窗内。原实现仅算头回拨一天, + // 不算头时 fetchAfter=开始日 会跳过首重置日取价、首段误用种子利率 + //(历史上靠 GetFloatRate 尾日取价回填种子掩盖;EQD-6968 自洽化后暴露并根治)。 + // "不算头少计一天"由计息边界 IncludeStart=false 承担,与此处无关。 + preEod.ValueDate = td.StartDate.Value.AddDays(-1); } return InitSwapDealInterest(td, valueDate, endDate, rate, position, add, swap, posiPrincipal, @@ -1359,10 +1372,10 @@ namespace YLErp.Modules.SwapModule { var resetDate = startDate.AddDays(i); bool needFetch = (fetchAfterDate == null || resetDate > fetchAfterDate.Value); - // 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息。 - // 从 relaxedFixingFromDate(缺省取 endDate)起的重置日:有价则取(满足重置日=平仓日用新利率), - // 缺价则跳过取价——currentFloat 保持不变(沿用上一重置日利率),不回退、不告警、不抛异常。 - // 算尾(calcLast=true)时所有重置日定盘照常强制取价。 + // 算头不算尾(calcLast=false)时,endDate 当天不计息,其重置日利率不参与计息—— + // 排除日(EQD-6968 自洽化)一概不取价(有价也不取),currentFloat 保持末段已消费利率: + // 事件/快照回写的浮动利率与金额同源、与平仓时刻无关。剩余持仓的新周期利率由 + // SwapEodPositionService 的"重置日再定盘"显式获取,不靠排除日顺带。算尾照常强制取价。 bool isExcludedEnd = !calcLast && resetDate >= (relaxedFixingFromDate ?? endDate); if (needFetch && !isExcludedEnd) { @@ -1370,19 +1383,8 @@ namespace YLErp.Modules.SwapModule } else if (needFetch && isExcludedEnd) { - // 有价则取新利率(重置日=平仓日时仍优先使用新利率);无价则跳过——该日利率不参与计息 - var fixingDate = IndexFixerBase.GetFixingDate(resetDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal r)) - { - currentFloat = r; - SwapCalcTrace.Critical( - $"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日有价→取新定盘(取价日={fixingDate:yyyy-MM-dd})={r:P6}"); - } - else - { - SwapCalcTrace.Critical( - $"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日缺价→跳过取价,沿用={currentFloat:P6}(该日不计息)"); - } + SwapCalcTrace.Critical( + $"FIX Segment p{position.id} {resetDate:yyyy-MM-dd} 排除日(不计息)→不取价,沿用末段={currentFloat:P6}"); } rates.Add((resetDate, spread + currentFloat)); } diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 11cdc20b..714ba36d 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -117,6 +117,14 @@ namespace YLErp.Modules.SwapModule req.ClosePosiNotionalValue, req.ClosePercent, req.EventType, req.TdClose, req.OrginPv, req.Add, settment: false, req.NewCalcLast, req.CloseList); + /// + /// 持仓延续腿重置日再定盘(EQD-6968 口径自洽化接缝)。 + /// 生产:Fr007IndexFixer.GetFixingOrThrow——缺价抛异常,与 ByEod 重置日再定盘/EodCheckSettlePrice + /// 同口径(EOD 时点当日 FR007 已由收盘前检查把关);测试:override 注入受控定盘。 + /// + protected virtual decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate) + => Fr007IndexFixer.Instance.GetFixingOrThrow(valueDate, position.interest_rule, position.FloatRateUnderlyingCode); + // FindTrade 已上提到基类 SwapTradeBaseService(三子类实现一致,消除重复) /// 查找交易扩展(生产: DbContext.trade_extend;测试: 内存字典) @@ -1361,6 +1369,20 @@ namespace YLErp.Modules.SwapModule //持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值: // ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。 CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0); + // ── 持仓延续腿重置日再定盘(EQD-6968 自洽化)── + // 排除日取价已收口为"纯跳过":事件利率=末段已消费利率。但剩余持仓自当日起进入新计息周期, + // 快照 FloatRate 是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的 + // 利率载体——平仓日恰为重置日时必须显式取当日新定盘(与 ByEod 日增路径的重置日行为同构)。 + // 全平(剩余=0)/算尾(事件利率已是新定盘)/观察日(autoSwap 恒1已含当日)无需再定盘。 + if (!autoSwap && !calcLast && posiNotionalValue > 0m + && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode) + && SwapDealService.IsResetDay(valueDate, td.StartDate.Value, position.interest_rest_days ?? 1)) + { + var ongoingFixing = ResolveOngoingResetFixing(position, valueDate); + SwapCalcTrace.Critical( + $"FIX EodCloseRefix p{position.id} {valueDate:yyyy-MM-dd} 平仓日=重置日→剩余持仓快照再定盘 {newEodPayPosition.FloatRate:P6}→{ongoingFixing:P6}"); + newEodPayPosition.FloatRate = ongoingFixing; + } //利息端估值用信息 // TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。 // 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。