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单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。